Support for MongoDB replica sets GH issue #86

This commit is contained in:
Chris Schaefer
2014-12-04 11:38:00 -05:00
parent f2be5e9430
commit 3f765c5515
8 changed files with 234 additions and 142 deletions

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ Servers
.gradle
_site
/bin
.idea
*.iml

View File

@@ -11,91 +11,103 @@ import org.springframework.cloud.util.UriInfoFactory;
*
*/
public abstract class UriBasedServiceInfo extends BaseServiceInfo {
private UriInfo uriInfo;
private UriInfo uriInfo;
private static UriInfoFactory uriFactory = new StandardUriInfoFactory();
private static UriInfoFactory uriFactory = new StandardUriInfoFactory();
public UriBasedServiceInfo(String id, String scheme, String host, int port, String username, String password, String path) {
super(id);
this.uriInfo = getUriInfoFactory().createUri(scheme, host, port, username, password, path);
this.uriInfo = validateAndCleanUriInfo(uriInfo);
}
public UriBasedServiceInfo(String id, String scheme, String host, int port, String username, String password, String path) {
super(id);
this.uriInfo = getUriInfoFactory().createUri(scheme, host, port, username, password, path);
this.uriInfo = validateAndCleanUriInfo(uriInfo);
}
public UriBasedServiceInfo(String id, String uriString) {
super(id);
this.uriInfo = getUriInfoFactory().createUri(uriString);
this.uriInfo = validateAndCleanUriInfo(uriInfo);
}
public UriBasedServiceInfo(String id, String uriString) {
super(id);
this.uriInfo = getUriInfoFactory().createUri(uriString);
this.uriInfo = validateAndCleanUriInfo(uriInfo);
}
/**
* For URI-based (@link ServiceInfo}s which don't conform to the standard URI
* format, override this method in your own ServiceInfo class to return a {@link UriInfoFactory} which will create the
* appropriate URIs.
*
* @return your special UriInfoFactory
*/
public UriInfoFactory getUriInfoFactory() {
return uriFactory;
}
/**
* For URI-based (@link ServiceInfo}s which don't conform to the standard URI
* format, override this method in your own ServiceInfo class to return a {@link UriInfoFactory} which will create the
* appropriate URIs.
*
* @return your special UriInfoFactory
*/
public UriInfoFactory getUriInfoFactory() {
return uriFactory;
}
@ServiceProperty(category = "connection")
public String getUri() {
return uriInfo.getUri().toString();
}
@ServiceProperty(category = "connection")
public String getUri() {
// converting a URI string that contains multiple hosts, ports, etc won't parse correctly.
// first attempt to use the parsed URI from the UriInfo, otherwise return the raw URI string
// that will be passed to the underlying driver / properties.
//
// TODO: either simply use URI strings or provide better support for URI's containing multiple hosts, etc.
//
if (uriInfo.getHost() != null) {
return uriInfo.getUri().toString();
}
@ServiceProperty(category = "connection")
public String getUserName() {
return uriInfo.getUserName();
}
return uriInfo.getRawUriString();
}
@ServiceProperty(category = "connection")
public String getPassword() {
return uriInfo.getPassword();
}
@ServiceProperty(category = "connection")
public String getUserName() {
return uriInfo.getUserName();
}
@ServiceProperty(category = "connection")
public String getHost() {
return uriInfo.getHost();
}
@ServiceProperty(category = "connection")
public String getPassword() {
return uriInfo.getPassword();
}
@ServiceProperty(category = "connection")
public int getPort() {
return uriInfo.getPort();
}
@ServiceProperty(category = "connection")
public String getHost() {
return uriInfo.getHost();
}
@ServiceProperty(category = "connection")
public String getPath() {
return uriInfo.getPath();
}
@ServiceProperty(category = "connection")
public int getPort() {
return uriInfo.getPort();
}
@ServiceProperty(category = "connection")
public String getQuery() {
return uriInfo.getQuery();
}
@ServiceProperty(category = "connection")
public String getPath() {
return uriInfo.getPath();
}
@ServiceProperty(category = "connection")
public String getScheme() {
return uriInfo.getScheme();
}
@ServiceProperty(category = "connection")
public String getQuery() {
return uriInfo.getQuery();
}
/**
* Validate the URI and clean it up by using defaults for any missing information, if possible.
*
* @param uriInfo
* uri info based on parsed payload
* @return cleaned up uri info
*/
protected UriInfo validateAndCleanUriInfo(UriInfo uriInfo) {
return uriInfo;
}
@ServiceProperty(category = "connection")
public String getScheme() {
return uriInfo.getScheme();
}
protected UriInfo getUriInfo() {
return uriInfo;
}
/**
* Validate the URI and clean it up by using defaults for any missing information, if possible.
*
* @param uriInfo
* uri info based on parsed payload
* @return cleaned up uri info
*/
protected UriInfo validateAndCleanUriInfo(UriInfo uriInfo) {
return uriInfo;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + getScheme() + "://" + getUserName() + ":****@" + getHost() + ":" + getPort()
+ "/" + getPath() + "]";
}
protected UriInfo getUriInfo() {
return uriInfo;
}
@Override
public String toString() {
// TODO: when using a simple URI string (see comments in getUri), the result of uriInfo.getRawUriString()
// would display the password which does not seem ideal.
return getClass().getSimpleName() + "[" + getScheme() + "://" + getUserName() + ":****@" + getHost() + ":" + getPort()
+ "/" + getPath() + "]";
}
}

View File

@@ -30,8 +30,7 @@ public class StandardUriInfoFactory implements UriInfoFactory {
String password = uriDecode(userInfo[1]);
return new UriInfo(tmpUri.getScheme(), tmpUri.getHost(), tmpUri.getPort(),
userName, password,
parsePath(tmpUri), tmpUri.getRawQuery());
userName, password, parsePath(tmpUri), tmpUri.getRawQuery(), uriString);
}
private URI createTmpUri(String uriString) {

View File

@@ -18,16 +18,20 @@ public class UriInfo {
private String path;
private URI uri;
private String query;
private String rawUriString;
public UriInfo(String scheme, String host, int port, String username, String password) {
this(scheme, host, port, username, password, "");
this(scheme, host, port, username, password,
String.format("%s://%s:%s@%s:%s/", scheme, username, password, host, port));
}
public UriInfo(String scheme, String host, int port, String username, String password, String path) {
this(scheme, host, port, username, password, path, null);
this(scheme, host, port, username, password, path, null,
String.format("%s://%s:%s@%s:%s/%s", scheme, username, password, host, port, path));
}
public UriInfo(String scheme, String host, int port, String username, String password, String path, String query) {
public UriInfo(String scheme, String host, int port, String username, String password, String path,
String query, String rawUriString) {
this.scheme = scheme;
this.host = host;
this.port = port;
@@ -35,6 +39,7 @@ public class UriInfo {
this.password = password;
this.path = path;
this.query = query;
this.rawUriString = rawUriString;
this.uri = buildUri();
}
@@ -71,6 +76,10 @@ public class UriInfo {
return uri;
}
public String getRawUriString() {
return rawUriString;
}
private URI buildUri() {
String userInfo = null;

View File

@@ -4,7 +4,10 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import com.mongodb.MongoClientURI;
import org.springframework.cloud.service.AbstractServiceConnectorCreator;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.ServiceConnectorCreationException;
@@ -27,22 +30,21 @@ import com.mongodb.WriteConcern;
*
* @author Ramnivas Laddad
* @author Thomas Risberg
*
* @author Chris Schaefer
*/
public class MongoDbFactoryCreator extends AbstractServiceConnectorCreator<MongoDbFactory, MongoServiceInfo> {
@Override
public MongoDbFactory create(MongoServiceInfo serviceInfo, ServiceConnectorConfig config) {
try {
MongoClientOptions mongoOptionsToUse = getMongoOptions((MongoDbFactoryConfig) config);
ServerAddress serverAddress = null;
if (serviceInfo.getPort() == -1) {
serverAddress = new ServerAddress(serviceInfo.getHost());
} else {
serverAddress = new ServerAddress(serviceInfo.getHost(), serviceInfo.getPort());
}
MongoClient mongo = new MongoClient(serverAddress, mongoOptionsToUse);
UserCredentials credentials = new UserCredentials(serviceInfo.getUserName(), serviceInfo.getPassword());
SimpleMongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(mongo, serviceInfo.getDatabase(), credentials);
MongoClientURI mongoClientURI = new MongoClientURI(serviceInfo.getUri());
List<ServerAddress> serverAddressList = getServerAddresses(mongoClientURI);
MongoClient mongo = new MongoClient(serverAddressList, mongoOptionsToUse);
UserCredentials credentials = new UserCredentials(mongoClientURI.getUsername(), new String(mongoClientURI.getPassword()));
SimpleMongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(mongo, mongoClientURI.getDatabase(), credentials);
return configure(mongoDbFactory, (MongoDbFactoryConfig) config);
} catch (UnknownHostException e) {
throw new ServiceConnectorCreationException(e);
@@ -51,43 +53,53 @@ public class MongoDbFactoryCreator extends AbstractServiceConnectorCreator<Mongo
}
}
private List<ServerAddress> getServerAddresses(MongoClientURI mongoClientURI) throws UnknownHostException {
List<String> servers = mongoClientURI.getHosts();
List<ServerAddress> serverAddressList = new ArrayList<ServerAddress>();
for(String server : servers) {
serverAddressList.add(new ServerAddress(server));
}
return serverAddressList;
}
private MongoClientOptions getMongoOptions(MongoDbFactoryConfig config) {
MongoClientOptions.Builder builder = null;
MongoClientOptions.Builder builder;
Method builderMethod = ClassUtils.getMethodIfAvailable(MongoClientOptions.class, "builder");
if (builderMethod != null) {
builder = (Builder) ReflectionUtils.invokeMethod(builderMethod, null);
builder = (Builder) ReflectionUtils.invokeMethod(builderMethod, null);
} else {
Constructor<Builder> builderConstructor = ClassUtils.getConstructorIfAvailable(MongoClientOptions.Builder.class);
try {
builder = builderConstructor.newInstance(new Object[0]);
} catch (InstantiationException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
Constructor<Builder> builderConstructor = ClassUtils.getConstructorIfAvailable(MongoClientOptions.Builder.class);
try {
builder = builderConstructor.newInstance(new Object[0]);
} catch (InstantiationException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
if (config != null) {
if (config.getConnectionsPerHost() != null) {
builder.connectionsPerHost(config.getConnectionsPerHost());
}
if (config.getMaxWaitTime() != null) {
builder.maxWaitTime(config.getMaxWaitTime());
}
if (config.getWriteConcern() != null) {
builder.writeConcern(new WriteConcern(config.getWriteConcern()));
}
}
if (config.getConnectionsPerHost() != null) {
builder.connectionsPerHost(config.getConnectionsPerHost());
}
if (config.getMaxWaitTime() != null) {
builder.maxWaitTime(config.getMaxWaitTime());
}
if (config.getWriteConcern() != null) {
builder.writeConcern(new WriteConcern(config.getWriteConcern()));
}
}
return builder.build();
}
public SimpleMongoDbFactory configure(SimpleMongoDbFactory mongoDbFactory, MongoDbFactoryConfig config) {
if (config != null && config.getWriteConcern() != null) {
WriteConcern writeConcern = WriteConcern.valueOf(config.getWriteConcern());
@@ -97,5 +109,4 @@ public class MongoDbFactoryCreator extends AbstractServiceConnectorCreator<Mongo
}
return mongoDbFactory;
}
}

View File

@@ -25,12 +25,12 @@ import org.springframework.cloud.service.common.RelationalServiceInfo;
import org.springframework.cloud.service.relational.MysqlDataSourceCreator;
/**
*
* @author Ramnivas Laddad
* Test cases for service properties
*
* @author Ramnivas Laddad
* @author Chris Schaefer
*/
public class CloudTest extends StubCloudConnectorTest {
private List<ServiceConnectorCreator<?, ? extends ServiceInfo>> serviceCreators;
@Before
@@ -74,7 +74,6 @@ public class CloudTest extends StubCloudConnectorTest {
assertNull(cloudProperties.get("cloud.services.mysql.connection.host"));
}
@Test
public void servicePropsOneServiceOfTheSameLabel() {
MysqlServiceInfo mysqlServiceInfo = createMysqlService("my-mysql");
@@ -101,7 +100,7 @@ public class CloudTest extends StubCloudConnectorTest {
assertBasicProps("cloud.services.my-redis", redisServiceInfo, cloudProperties);
assertBasicProps("cloud.services.redis", redisServiceInfo, cloudProperties);
}
@Test
public void servicePropsRabbit() {
String serviceId = "my-rabbit";
@@ -113,6 +112,28 @@ public class CloudTest extends StubCloudConnectorTest {
assertRabbitProps("cloud.services.my-rabbit", rabbitServiceInfo, cloudProperties);
assertRabbitProps("cloud.services.rabbitmq", rabbitServiceInfo, cloudProperties);
}
@Test
public void servicePropsMongoMultipleHostsUriString() {
String serviceId = "my-mongo-multiple-hosts-uri";
MongoServiceInfo mongoServiceInfo = createMongoServiceWithMultipleHostsByUri(serviceId);
CloudConnector stubCloudConnector = getTestCloudConnector(mongoServiceInfo);
Cloud testCloud = new Cloud(stubCloudConnector, serviceCreators);
Properties cloudProperties = testCloud.getCloudProperties();
assertMongoPropsWithMultipleHostsByUri("cloud.services.my-mongo-multiple-hosts-uri", mongoServiceInfo, cloudProperties);
assertMongoPropsWithMultipleHostsByUri("cloud.services.mongo", mongoServiceInfo, cloudProperties);
}
private void assertMongoPropsWithMultipleHostsByUri(String leadKey, MongoServiceInfo serviceInfo, Properties cloudProperties) {
assertEquals(serviceInfo.getId(), cloudProperties.get(leadKey + ".id"));
assertEquals(serviceInfo.getUri(), cloudProperties.get(leadKey + ".connection.uri"));
assertEquals(-1, cloudProperties.get(leadKey + ".connection.port"));
assertNull(cloudProperties.get(leadKey + ".connection.host"));
assertNull(cloudProperties.get(leadKey + ".connection.username"));
assertNull(cloudProperties.get(leadKey + ".connection.password"));
}
private void assertBasicProps(String leadKey, UriBasedServiceInfo serviceInfo, Properties cloudProperties) {
assertEquals(serviceInfo.getId(), cloudProperties.get(leadKey + ".id"));

View File

@@ -15,10 +15,9 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
* Base class for close-to-integration tests that use a stub {@link CloudConnector} to avoid the need for a real cloud environment.
*
* @author Ramnivas Laddad
*
* @author Chris Schaefer
*/
abstract public class StubCloudConnectorTest {
private static final String MOCK_CLOUD_BEAN_NAME = "mockCloud";
protected ApplicationContext getTestApplicationContext(String fileName, ServiceInfo... serviceInfos) {
@@ -63,7 +62,11 @@ abstract public class StubCloudConnectorTest {
protected MongoServiceInfo createMongoService(String id) {
return new MongoServiceInfo(id, "10.20.30.40", 1234, "username", "password", "db");
}
protected MongoServiceInfo createMongoServiceWithMultipleHostsByUri(String id) {
return new MongoServiceInfo(id, "mongo://username:password@10.20.30.40,10.20.30.41,10.20.30.42:1234/db");
}
protected AmqpServiceInfo createRabbitService(String id) {
return new AmqpServiceInfo(id, "10.20.30.40", 1234, "username", "password", "vh");
}

View File

@@ -14,53 +14,88 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.Mongo;
import com.mongodb.ServerAddress;
import org.springframework.util.StringUtils;
/**
*
* @author Ramnivas Laddad
* Test cases for Mongo service connector creators.
*
* @author Ramnivas Laddad
* @author Chris Schaefer
*/
public class MongoServiceConnectorCreatorTest {
private static final String TEST_HOST = "10.20.30.40";
private static final String TEST_HOST_1 = "10.20.30.41";
private static final String TEST_HOST_2 = "10.20.30.42";
private static final int TEST_PORT = 1234;
private static final int TEST_PORT_DEFAULT = 27017;
private static final String TEST_USERNAME = "myuser";
private static final String TEST_PASSWORD = "mypass";
private static final String TEST_DB = "mydb";
private static final String MONGODB_SCHEME = "mongodb";
private static final String[] TEST_HOSTS = new String[] { TEST_HOST, TEST_HOST_1, TEST_HOST_2 };
private MongoDbFactoryCreator testCreator = new MongoDbFactoryCreator();
@Test
public void cloudMongoCreationNoConfig() throws Exception {
MongoServiceInfo serviceInfo = createServiceInfo();
MongoServiceInfo serviceInfo = new MongoServiceInfo("id", TEST_HOST, TEST_PORT, TEST_USERNAME, TEST_PASSWORD, TEST_DB);
MongoDbFactory mongoDbFactory = testCreator.create(serviceInfo, null);
assertConnectorProperties(serviceInfo, mongoDbFactory);
}
assertNotNull(mongoDbFactory);
public MongoServiceInfo createServiceInfo() {
return new MongoServiceInfo("id", TEST_HOST, TEST_PORT, TEST_DB, TEST_USERNAME, TEST_PASSWORD);
}
private void assertConnectorProperties(MongoServiceInfo serviceInfo, MongoDbFactory connector) {
assertNotNull(connector);
Mongo mongo = (Mongo) ReflectionTestUtils.getField(connector, "mongo");
UserCredentials credentials = (UserCredentials) ReflectionTestUtils.getField(connector, "credentials");
Mongo mongo = (Mongo) ReflectionTestUtils.getField(mongoDbFactory, "mongo");
UserCredentials credentials = (UserCredentials) ReflectionTestUtils.getField(mongoDbFactory, "credentials");
assertNotNull(mongo);
List<ServerAddress> addresses = mongo.getAllAddress();
assertEquals(1, addresses.size());
ServerAddress address = addresses.get(0);
assertEquals(serviceInfo.getHost(), address.getHost());
assertEquals(serviceInfo.getPort(), address.getPort());
assertEquals(serviceInfo.getUserName(), ReflectionTestUtils.getField(credentials, "username"));
assertEquals(serviceInfo.getPassword(), ReflectionTestUtils.getField(credentials, "password"));
// Don't do connector.getDatabase().getName() as that will try to initiate the connection
assertEquals(serviceInfo.getDatabase(), ReflectionTestUtils.getField(connector, "databaseName"));
assertEquals(serviceInfo.getDatabase(), ReflectionTestUtils.getField(mongoDbFactory, "databaseName"));
}
@Test
public void cloudMongoCreationWithMultipleHostsByUri() throws Exception {
String uri = String.format("%s://%s:%s@%s:%s/%s", MONGODB_SCHEME, TEST_USERNAME, TEST_PASSWORD,
StringUtils.arrayToDelimitedString(TEST_HOSTS, ","), TEST_PORT, TEST_DB);
MongoServiceInfo serviceInfo = new MongoServiceInfo("id", uri);
MongoDbFactory mongoDbFactory = testCreator.create(serviceInfo, null);
assertNotNull(mongoDbFactory);
Mongo mongo = (Mongo) ReflectionTestUtils.getField(mongoDbFactory, "mongo");
UserCredentials credentials = (UserCredentials) ReflectionTestUtils.getField(mongoDbFactory, "credentials");
assertNotNull(mongo);
List<ServerAddress> addresses = mongo.getAllAddress();
assertEquals(3, addresses.size());
assertEquals(TEST_USERNAME, ReflectionTestUtils.getField(credentials, "username"));
assertEquals(TEST_PASSWORD, ReflectionTestUtils.getField(credentials, "password"));
// Don't do connector.getDatabase().getName() as that will try to initiate the connection
assertEquals(TEST_DB, ReflectionTestUtils.getField(mongoDbFactory, "databaseName"));
ServerAddress address1 = addresses.get(0);
assertEquals(TEST_HOST, address1.getHost());
assertEquals(TEST_PORT_DEFAULT, address1.getPort());
ServerAddress address2 = addresses.get(1);
assertEquals(TEST_HOST_1, address2.getHost());
assertEquals(TEST_PORT_DEFAULT, address2.getPort());
ServerAddress address3 = addresses.get(2);
assertEquals(TEST_HOST_2, address3.getHost());
assertEquals(TEST_PORT, address3.getPort());
}
}