Skip to content

Commit e7429d5

Browse files
j-pottsmarkglh
authored andcommitted
Add replication strategy support (#1)
* Basic setup for replication configuration support for Pillar. * Removed ReplicationOptions as it was redundant. * Error handling for reading ReplicationStrategy from our config. * Tweaked the error handling. * Modified the CommandExecutorTest and added a new test. * Add qualifications for config params. * added cassandra unit and tweaked app default params * Improved README
1 parent 81566a3 commit e7429d5

25 files changed

+1125
-128
lines changed

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,3 @@ jdk:
44
- oraclejdk8
55
scala:
66
- 2.11.8
7-
services:
8-
- cassandra

README.md

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,32 @@ authored. Migrations are applied in ascending order and reversed in descending o
6969

7070
### Command Line
7171

72-
Here's the short version:
72+
#####Here's the short version:
7373

74+
Given the configuration:
75+
76+
```
77+
pillar.my_keyspace {
78+
prod {
79+
...
80+
}
81+
development {
82+
...
83+
}
84+
}
85+
```
7486
1. Write migrations, place them in conf/pillar/migrations/myapp.
7587
1. Add pillar settings to conf/application.conf.
76-
1. % pillar initialize myapp
77-
1. % pillar migrate myapp
88+
1. `% pillar initialize -e prod my_keyspace`
89+
1. `% pillar migrate -e prod my_keyspace`
90+
91+
*Note: development is the default environment if nothing is specified*
92+
93+
Or we could compile and run the jar:
94+
95+
```
96+
java -cp "slf4j-simple.jar:pillar-assembly.jar" de.kaufhof.pillar.cli.App -d "path/to/migrations" -e "prod" initialize "my_keyspace"
97+
```
7898

7999
#### Migration Files
80100

@@ -136,26 +156,37 @@ The Pillar command line interface expects to find migrations in conf/pillar/migr
136156
#### Configuration
137157

138158
Pillar uses the [Typesafe Config][typesafeconfig] library for configuration. The Pillar command-line interface expects
139-
to find an application.conf file in ./conf or ./src/main/resources. Given a data store called faker, the
140-
application.conf might look like the following:
159+
to find an application.conf file in ./conf or ./src/main/resources.
160+
The ReplicationStrategy and ReplicationFactor can be configured per environment. If left out completely,
161+
SimplyStrategy with RF 3 will be used by default.
162+
Given a data store called faker, the application.conf might look like the following:
141163

164+
```
142165
pillar.faker {
143166
development {
144167
cassandra-seed-address: "127.0.0.1"
145168
cassandra-keyspace-name: "pillar_development"
169+
replicationStrategy: "SimpleStrategy"
170+
replicationFactor: 0
146171
}
147-
test {
148-
cassandra-seed-address: "127.0.0.1"
149-
cassandra-keyspace-name: "pillar_test"
150-
}
151-
acceptance_test {
172+
}
173+
```
174+
```
175+
pillar.faker {
176+
development {
152177
cassandra-seed-address: "127.0.0.1"
153-
cassandra-keyspace-name: "pillar_acceptance_test"
178+
cassandra-keyspace-name: "pillar_development"
179+
replicationStrategy: "NetworkTopologyStrategy"
180+
replicationFactor: [
181+
{dc1: 2},
182+
{dc2: 3}
183+
]
154184
}
155185
}
186+
```
156187

157188
##### SSL & Authentication
158-
You can add ssl options and authentication to each of the environments:
189+
You can optionally add ssl options and authentication to each of the environments:
159190

160191
pillar.faker {
161192
development {
@@ -225,7 +256,7 @@ The package installs to /opt/pillar by default. The /opt/pillar/bin/pillar execu
225256

226257
data-store The target data store, as defined in application.conf
227258

228-
#### Examples
259+
#### More Examples
229260

230261
Initialize the faker datastore development environment
231262

project/PillarBuild.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ object PillarBuild extends Build {
2020
"com.typesafe" % "config" % "1.0.1",
2121
"org.clapper" %% "argot" % "1.0.3",
2222
"org.mockito" % "mockito-core" % "1.9.5" % "test",
23-
"org.scalatest" %% "scalatest" % "2.2.0" % "test"
23+
"org.scalatest" %% "scalatest" % "2.2.0" % "test",
24+
"org.cassandraunit" % "cassandra-unit" % "3.0.0.1" % "test",
25+
"com.google.guava" % "guava" % "18.0" % "test",
26+
"ch.qos.logback" % "logback-classic" % "1.1.7" % "test"
2427
)
2528

2629
val rhPackage = TaskKey[File]("rh-package", "Packages the application for Red Hat Package Manager")
@@ -80,6 +83,7 @@ object PillarBuild extends Build {
8083
else
8184
Some("releases" at nexus + "service/local/staging/deploy/maven2")
8285
},
86+
parallelExecution in Test := false,
8387
publishMavenStyle := true,
8488
publishArtifact in Test := false,
8589
pomIncludeRepository := { _ => false },

src/main/resources/application.conf

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,72 @@ pillar.faker {
77
test {
88
cassandra-seed-address: "127.0.0.1"
99
cassandra-keyspace-name: "pillar_test"
10+
replicationStrategy: "NetworkTopologyStrategy"
11+
replicationFactor: [
12+
{dc1: 2},
13+
{dc2: 3}
14+
]
1015
}
1116
acceptance_test {
1217
cassandra-seed-address: "127.0.0.1"
1318
cassandra-keyspace-name: "pillar_acceptance_test"
19+
replicationStrategy: "SimpleStrategy"
20+
replicationFactor: 1
1421
}
15-
}
22+
}
23+
24+
pillar.test {
25+
simpleGood {
26+
cassandra-seed-address: "127.0.0.1"
27+
cassandra-keyspace-name: "pillar_test"
28+
replicationStrategy: "SimpleStrategy"
29+
replicationFactor: 1
30+
}
31+
simpleBadStrat {
32+
cassandra-seed-address: "127.0.0.1"
33+
cassandra-keyspace-name: "pillar_test"
34+
replicationStrategy: "SimpleStrategee"
35+
replicationFactor: 1
36+
}
37+
simpleBadRep {
38+
cassandra-seed-address: "127.0.0.1"
39+
cassandra-keyspace-name: "pillar_test"
40+
replicationStrategy: "SimpleStrategy"
41+
replicationFactor: foo
42+
}
43+
simpleMissingRep {
44+
cassandra-seed-address: "127.0.0.1"
45+
cassandra-keyspace-name: "pillar_test"
46+
replicationStrategy: "SimpleStrategy"
47+
}
48+
simpleZeroRep {
49+
cassandra-seed-address: "127.0.0.1"
50+
cassandra-keyspace-name: "pillar_test"
51+
replicationStrategy: "SimpleStrategy"
52+
replicationFactor: 0
53+
}
54+
netGood {
55+
cassandra-seed-address: "127.0.0.1"
56+
cassandra-keyspace-name: "pillar_test"
57+
replicationStrategy: "NetworkTopologyStrategy"
58+
replicationFactor: [
59+
{dc1: 2},
60+
{dc2: 3}
61+
]
62+
}
63+
netEmptyRep {
64+
cassandra-seed-address: "127.0.0.1"
65+
cassandra-keyspace-name: "pillar_test"
66+
replicationStrategy: "NetworkTopologyStrategy"
67+
replicationFactor: []
68+
}
69+
netZeroRep {
70+
cassandra-seed-address: "127.0.0.1"
71+
cassandra-keyspace-name: "pillar_test"
72+
replicationStrategy: "NetworkTopologyStrategy"
73+
replicationFactor: [
74+
{dc1: 0},
75+
{dc2: 3}
76+
]
77+
}
78+
}

src/main/scala/de/kaufhof/pillar/CassandraMigrator.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class CassandraMigrator(registry: Registry) extends Migrator {
1212
selectMigrationsToApply(dateRestriction, appliedMigrations).foreach(_.executeUpStatement(session))
1313
}
1414

15-
override def initialize(session: Session, keyspace: String, replicationOptions: ReplicationOptions = ReplicationOptions.default) {
16-
executeIdempotentCommand(session, "CREATE KEYSPACE %s WITH replication = %s".format(keyspace, replicationOptions.toString()))
15+
override def initialize(session: Session, keyspace: String, replicationStrategy: ReplicationStrategy) {
16+
executeIdempotentCommand(session, s"CREATE KEYSPACE $keyspace WITH replication = ${replicationStrategy.cql}")
1717
executeIdempotentCommand(session,
1818
"""
1919
| CREATE TABLE %s.applied_migrations (

src/main/scala/de/kaufhof/pillar/Migrator.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ object Migrator {
1717
trait Migrator {
1818
def migrate(session: Session, dateRestriction: Option[Date] = None)
1919

20-
def initialize(session: Session, keyspace: String, replicationOptions: ReplicationOptions = ReplicationOptions.default)
20+
def initialize(session: Session, keyspace: String, replicationStrategy: ReplicationStrategy = SimpleStrategy())
2121

2222
def destroy(session: Session, keyspace: String)
2323
}

src/main/scala/de/kaufhof/pillar/PrintStreamReporter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import java.util.Date
66
import com.datastax.driver.core.Session
77

88
class PrintStreamReporter(stream: PrintStream) extends Reporter {
9-
override def initializing(session: Session, keyspace: String, replicationOptions: ReplicationOptions) {
9+
override def initializing(session: Session, keyspace: String, replicationStrategy: ReplicationStrategy) {
1010
stream.println(s"Initializing $keyspace")
1111
}
1212

src/main/scala/de/kaufhof/pillar/ReplicationOptions.scala

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package de.kaufhof.pillar
2+
3+
/**
4+
* Defines all possible ReplicationStrategy configurations.
5+
* A NetworkTopologyStrategy will require the appropriate snitch.
6+
*/
7+
sealed trait ReplicationStrategy {
8+
def cql: String
9+
override def toString: String = cql
10+
}
11+
12+
final case class SimpleStrategy(replicationFactor: Int = 3) extends ReplicationStrategy {
13+
require(replicationFactor > 0)
14+
15+
override def cql: String = s"{'class' : 'SimpleStrategy', 'replication_factor' : $replicationFactor}"
16+
}
17+
18+
final case class NetworkTopologyStrategy(dataCenters: Seq[CassandraDataCenter]) extends ReplicationStrategy {
19+
require(dataCenters.nonEmpty)
20+
21+
override def cql: String = {
22+
val replicationFacString = dataCenters.map { dc =>
23+
s"'${dc.name}' : ${dc.replicationFactor} "
24+
}.mkString(", ")
25+
26+
s"{'class' : 'NetworkTopologyStrategy', $replicationFacString }"
27+
}
28+
}
29+
30+
final case class CassandraDataCenter(name: String, replicationFactor: Int){
31+
require(replicationFactor > 0 && name.nonEmpty)
32+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package de.kaufhof.pillar
2+
3+
import java.util.Map.Entry
4+
5+
import com.typesafe.config.ConfigException.BadValue
6+
import com.typesafe.config.{Config, ConfigException, ConfigObject, ConfigValue}
7+
8+
import scala.util.{Failure, Success, Try}
9+
10+
final case class ReplicationStrategyConfigError(msg: String) extends Exception
11+
12+
object ReplicationStrategyBuilder {
13+
14+
/**
15+
* Parses replication settings from a config that looks like:
16+
* {{{
17+
* replicationStrategy: "SimpleStrategy"
18+
* replicationFactor: 3
19+
* }}}
20+
*
21+
* or:
22+
*
23+
* {{{
24+
* replicationStrategy: "NetworkTopologyStrategy"
25+
* replicationFactor: [
26+
* {dc1: 3},
27+
* {dc2: 3}
28+
* ]
29+
* }}}
30+
*
31+
* @param configuration The applications Typesafe config
32+
* @param dataStoreName The target data store, as defined in application.conf
33+
* @param environment The environment, as defined in application.conf (i.e. "pillar.dataStoreName.environment {...})
34+
* @return ReplicationOptions with a default of Simple Strategy with a replication factor of 3.
35+
*/
36+
def getReplicationStrategy(configuration: Config, dataStoreName: String, environment: String): ReplicationStrategy = try {
37+
38+
val repStrategyStr = Try(configuration.getString(s"pillar.$dataStoreName.$environment.replicationStrategy"))
39+
40+
repStrategyStr match {
41+
case Success(repStrategy) => repStrategy match {
42+
case "SimpleStrategy" =>
43+
val repFactor = configuration.getInt(s"pillar.$dataStoreName.$environment.replicationFactor")
44+
SimpleStrategy(repFactor)
45+
46+
case "NetworkTopologyStrategy" =>
47+
import scala.collection.JavaConverters._
48+
val dcConfigBuffer = configuration
49+
.getObjectList(s"pillar.$dataStoreName.$environment.replicationFactor")
50+
.asScala
51+
52+
val dcBuffer = for {
53+
item: ConfigObject <- dcConfigBuffer
54+
entry: Entry[String, ConfigValue] <- item.entrySet().asScala
55+
dcName = entry.getKey
56+
dcRepFactor = entry.getValue.unwrapped().toString.toInt
57+
} yield (dcName, dcRepFactor)
58+
59+
val datacenters = dcBuffer
60+
.map(dc => CassandraDataCenter(dc._1, dc._2))
61+
.toList
62+
63+
NetworkTopologyStrategy(datacenters)
64+
65+
case _ =>
66+
throw new ReplicationStrategyConfigError(s"$repStrategy is not a valid replication strategy.")
67+
}
68+
69+
case Failure(e: ConfigException.Missing) => SimpleStrategy()
70+
case Failure(e) => throw e
71+
}
72+
} catch {
73+
case e: IllegalArgumentException => throw new BadValue(s"pillar.$dataStoreName.$environment", e.getMessage)
74+
case e: Exception => throw e
75+
}
76+
}

0 commit comments

Comments
 (0)