From 84d0530419ef803c1ee62c3bdb685b42d6aa082f Mon Sep 17 00:00:00 2001 From: thesquelched Date: Wed, 12 Apr 2017 13:39:47 -0500 Subject: [PATCH 1/2] Add support for consul shared configuration Add consul as a shared config provider. Fixes #280 --- exhibitor-core/pom.xml | 11 ++ .../config/consul/ConsulConfigProvider.java | 128 ++++++++++++++++++ .../core/config/consul/ConsulKvLock.java | 60 ++++++++ .../core/config/consul/ConsulPseudoLock.java | 33 +++++ .../consul/ConsulVersionedProperties.java | 21 +++ .../consul/TestConsulConfigProvider.java | 65 +++++++++ .../exhibitor/standalone/ExhibitorCLI.java | 21 ++- .../standalone/ExhibitorCreator.java | 63 +++++++++ .../standalone/NullHostnameVerifier.java | 11 ++ pom.xml | 14 ++ 10 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulConfigProvider.java create mode 100644 exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java create mode 100644 exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulPseudoLock.java create mode 100644 exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulVersionedProperties.java create mode 100644 exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java create mode 100644 exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/NullHostnameVerifier.java diff --git a/exhibitor-core/pom.xml b/exhibitor-core/pom.xml index 1e9811f7..9c0f155b 100644 --- a/exhibitor-core/pom.xml +++ b/exhibitor-core/pom.xml @@ -94,6 +94,11 @@ jersey-json + + com.orbitz.consul + consul-client + + org.apache.curator curator-test @@ -111,5 +116,11 @@ testng test + + + com.pszymczyk.consul + embedded-consul + test + diff --git a/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulConfigProvider.java b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulConfigProvider.java new file mode 100644 index 00000000..b7e79801 --- /dev/null +++ b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulConfigProvider.java @@ -0,0 +1,128 @@ +package com.netflix.exhibitor.core.config.consul; + +import com.netflix.exhibitor.core.config.*; +import com.orbitz.consul.Consul; +import com.orbitz.consul.KeyValueClient; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class ConsulConfigProvider implements ConfigProvider { + private static final Long DEFAULT_LOCK_TIMEOUT_MS = 5L * 60L * 1000L; // 5 minutes; + private final Consul consul; + private final Properties defaults; + private final String basePath; + private final String versionPath; + private final String propertiesPath; + private final ConsulKvLock lock; + private final String pseudoLockPath; + private final Long lockTimeoutMs; + + /** + * @param consul consul client instance for connecting to consul cluster + * @param prefix consul key-value path under which configs are stored + * @param defaults default properties + */ + public ConsulConfigProvider(Consul consul, String prefix, Properties defaults) { + this(consul, prefix, defaults, DEFAULT_LOCK_TIMEOUT_MS); + } + + /** + * @param consul consul client instance for connecting to consul cluster + * @param prefix consul key-value path under which configs are stored + * @param defaults default properties + * @param lockTimeoutMs timeout, in milliseconds, for lock acquisition + */ + public ConsulConfigProvider(Consul consul, String prefix, Properties defaults, Long lockTimeoutMs) { + this.consul = consul; + this.defaults = defaults; + this.lockTimeoutMs = lockTimeoutMs; + + this.basePath = prefix.endsWith("/") ? prefix : prefix + "/"; + this.versionPath = basePath + "version"; + this.propertiesPath = basePath + "properties"; + this.pseudoLockPath = basePath + "pseudo-locks"; + + this.lock = new ConsulKvLock(consul, basePath + "lock", "exhibitor"); + } + + @Override + public void start() throws Exception { + // NOP + } + + @Override + public void close() throws IOException { + // NOP + } + + @Override + public LoadedInstanceConfig loadConfig() throws Exception { + ConsulVersionedProperties properties; + + lock.acquireLock(lockTimeoutMs, TimeUnit.MILLISECONDS); + try { + properties = loadProperties(); + } + finally { + lock.releaseLock(); + } + + PropertyBasedInstanceConfig config = new PropertyBasedInstanceConfig( + properties.getProperties(), defaults); + return new LoadedInstanceConfig(config, properties.getVersion()); + } + + @Override + public LoadedInstanceConfig storeConfig(ConfigCollection config, long compareVersion) throws Exception { + Long currentVersion = loadProperties().getVersion(); + if (currentVersion != compareVersion) { + return null; + } + + KeyValueClient kv = consul.keyValueClient(); + PropertyBasedInstanceConfig instanceConfig = new PropertyBasedInstanceConfig(config); + StringWriter writer = new StringWriter(); + instanceConfig.getProperties().store(writer, "Auto-generated by Exhibitor"); + + lock.acquireLock(lockTimeoutMs, TimeUnit.MILLISECONDS); + try { + kv.putValue(propertiesPath, writer.toString()); + kv.putValue(versionPath, String.valueOf(currentVersion + 1)); + } + finally { + lock.releaseLock(); + } + + return new LoadedInstanceConfig(instanceConfig, currentVersion + 1); + } + + @Override + public PseudoLock newPseudoLock() throws Exception { + return new ConsulPseudoLock(consul, pseudoLockPath); + } + + private String getString(String path) { + return consul.keyValueClient().getValueAsString(path).orNull(); + } + + private Long getLong(String path) { + return Long.valueOf(consul.keyValueClient().getValueAsString(path).or("0")); + } + + private ConsulVersionedProperties loadProperties() throws Exception { + Long version = getLong(versionPath); + + Properties properties = new Properties(); + String rawProperties = getString(propertiesPath); + if (rawProperties != null) { + StringReader reader = new StringReader(getString(propertiesPath)); + properties.load(reader); + } + + return new ConsulVersionedProperties(properties, version); + } +} diff --git a/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java new file mode 100644 index 00000000..912eeda9 --- /dev/null +++ b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java @@ -0,0 +1,60 @@ +package com.netflix.exhibitor.core.config.consul; + +import com.google.common.base.Optional; +import com.orbitz.consul.Consul; +import com.orbitz.consul.KeyValueClient; +import com.orbitz.consul.model.kv.Value; +import com.orbitz.consul.model.session.ImmutableSession; +import com.orbitz.consul.option.QueryOptions; + +import java.math.BigInteger; +import java.util.concurrent.TimeUnit; + +public class ConsulKvLock { + private final Consul consul; + private final String path; + private final String name; + + /** + * @param consul consul client instance for connecting to consul cluster + * @param path consul key-value path to lock + * @param name a descriptive name for the lock + */ + public ConsulKvLock(Consul consul, String path, String name) { + this.consul = consul; + this.path = path; + this.name = name; + } + + private String createSession() { + final ImmutableSession session = ImmutableSession.builder().name(name).build(); + return consul.sessionClient().createSession(session).getId(); + } + + public boolean acquireLock(long maxWait, TimeUnit unit) { + KeyValueClient kv = consul.keyValueClient(); + String sessionId = createSession(); + + Optional value = kv.getValue(path); + + if (kv.acquireLock(path, sessionId)) { + return true; + } + + BigInteger index = BigInteger.valueOf(value.get().getModifyIndex()); + kv.getValue(path, QueryOptions.blockMinutes((int) unit.toMinutes(maxWait), index).build()); + return kv.acquireLock(path, sessionId); + } + + public void releaseLock() { + KeyValueClient kv = consul.keyValueClient(); + Optional value = kv.getValue(path); + + if (value.isPresent()) { + Optional session = value.get().getSession(); + if (session.isPresent()) { + kv.releaseLock(path, session.get()); + } + } + } +} diff --git a/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulPseudoLock.java b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulPseudoLock.java new file mode 100644 index 00000000..cdeaf18c --- /dev/null +++ b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulPseudoLock.java @@ -0,0 +1,33 @@ +package com.netflix.exhibitor.core.config.consul; + +import com.netflix.exhibitor.core.activity.ActivityLog; +import com.netflix.exhibitor.core.config.PseudoLock; +import com.orbitz.consul.Consul; + +import java.util.concurrent.TimeUnit; + +public class ConsulPseudoLock implements PseudoLock { + + private final ConsulKvLock lock; + + public ConsulPseudoLock(Consul consul, String prefix) { + String path = prefix.endsWith("/") ? prefix + "pseudo-lock" : prefix + "/pseudo-lock"; + this.lock = new ConsulKvLock(consul, path, "pseudo-lock"); + } + + @Override + public boolean lock(ActivityLog log, long maxWait, TimeUnit unit) throws Exception { + if (!lock.acquireLock(maxWait, unit)) { + log.add(ActivityLog.Type.ERROR, + String.format("Could not acquire lock within %d ms", unit.toMillis(maxWait))); + return false; + } + + return true; + } + + @Override + public void unlock() throws Exception { + lock.releaseLock(); + } +} diff --git a/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulVersionedProperties.java b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulVersionedProperties.java new file mode 100644 index 00000000..da291bb0 --- /dev/null +++ b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulVersionedProperties.java @@ -0,0 +1,21 @@ +package com.netflix.exhibitor.core.config.consul; + +import java.util.Properties; + +public class ConsulVersionedProperties { + private final Properties properties; + private final Long version; + + public ConsulVersionedProperties(Properties properties, Long version) { + this.properties = properties; + this.version = version; + } + + public Properties getProperties() { + return properties; + } + + public Long getVersion() { + return version; + } +} diff --git a/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java b/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java new file mode 100644 index 00000000..3eaa0697 --- /dev/null +++ b/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java @@ -0,0 +1,65 @@ +package com.netflix.exhibitor.core.config.consul; + +import com.google.common.net.HostAndPort; +import com.netflix.exhibitor.core.config.LoadedInstanceConfig; +import com.netflix.exhibitor.core.config.PropertyBasedInstanceConfig; +import com.netflix.exhibitor.core.config.StringConfigs; +import com.orbitz.consul.Consul; +import com.pszymczyk.consul.ConsulProcess; +import com.pszymczyk.consul.ConsulStarterBuilder; +import org.apache.curator.test.Timing; +import org.apache.curator.utils.CloseableUtils; +import org.testng.Assert; +import org.testng.annotations.*; + +import java.util.Properties; + + +public class TestConsulConfigProvider { + private Timing timing; + private ConsulProcess consul; + private Consul client; + + @BeforeClass + public void setupClass() throws Exception { + consul = ConsulStarterBuilder.consulStarter().build().start(); + } + + @AfterClass + public void tearDownClass() + { + consul.close(); + } + + @BeforeMethod + public void setup() throws Exception { + timing = new Timing(); + + consul.reset(); + client = Consul.builder() + .withHostAndPort(HostAndPort.fromParts("localhost", consul.getHttpPort())) + .build(); + } + + @Test + public void testBasic() throws Exception { + ConsulConfigProvider config = new ConsulConfigProvider(client, "prefix", new Properties()); + + try { + config.start(); + config.loadConfig(); + + Properties properties = new Properties(); + properties.setProperty(PropertyBasedInstanceConfig.toName(StringConfigs.ZOO_CFG_EXTRA, PropertyBasedInstanceConfig.ROOT_PROPERTY_PREFIX), "1,2,3"); + config.storeConfig(new PropertyBasedInstanceConfig(properties, new Properties()), 0); + + timing.sleepABit(); + + LoadedInstanceConfig instanceConfig = config.loadConfig(); + Assert.assertEquals(instanceConfig.getConfig().getRootConfig().getString(StringConfigs.ZOO_CFG_EXTRA), "1,2,3"); + } + finally { + CloseableUtils.closeQuietly(config); + } + } +} \ No newline at end of file diff --git a/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCLI.java b/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCLI.java index 3ee5febb..3e31a6f1 100644 --- a/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCLI.java +++ b/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCLI.java @@ -79,6 +79,14 @@ private OptionSection(String sectionName, Options options) public static final String ZOOKEEPER_CONFIG_POLLING = "zkconfigpollms"; public static final String NONE_CONFIG_DIRECTORY = "noneconfigdir"; public static final String INITIAL_CONFIG_FILE = "defaultconfig"; + public static final String CONSUL_CONFIG_HOST = "consulhost"; + public static final String CONSUL_CONFIG_PORT = "consulport"; + public static final String CONSUL_CONFIG_KEY_PREFIX = "consulprefix"; + public static final String CONSUL_CONFIG_ACL_TOKEN = "consulacltoken"; + public static final String CONSUL_CONFIG_SSL = "consulssl"; + public static final String CONSUL_CONFIG_SSL_VERIFY_HOSTNAME = "consulsslverifyhostname"; + public static final String CONSUL_CONFIG_SSL_PROTOCOL = "consulsslprotocol"; + public static final String CONSUL_CONFIG_SSL_CA_CERT = "consulsslcacert"; public static final String FILESYSTEMBACKUP = "filesystembackup"; public static final String TIMEOUT = "timeout"; @@ -158,6 +166,16 @@ public ExhibitorCLI() s3Options.addOption(null, S3_REGION, true, "Optional region for S3 calls (e.g. \"eu-west-1\"). Will be used to set the S3 client's endpoint."); s3Options.addOption(null, S3_PROXY, true, "Optional configuration used when when connecting to S3 via a proxy. Argument is the path to an AWS credential properties file with four properties (only host, port and protocol are required if using a proxy): " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_HOST + ", " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_PORT + ", " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_USERNAME + ", " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_PASSWORD); + Options consulConfigOptions = new Options(); + consulConfigOptions.addOption(null, CONSUL_CONFIG_HOST, true, "Consul host; defaults to \"localhost\""); + consulConfigOptions.addOption(null, CONSUL_CONFIG_PORT, true, "Consul HTTP(s) port; defaults to 8500"); + consulConfigOptions.addOption(null, CONSUL_CONFIG_ACL_TOKEN, true, "Optional Consul ACL token"); + consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL, true, "If true, enables Consul communication over SSL"); + consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL_VERIFY_HOSTNAME, true, "If true, verify SSL hostnames"); + consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL_PROTOCOL, true, "Consul SSL/TLS protocol; defaults to \"TLSv1.2\""); + consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL_CA_CERT, true, "Path to the consul CA cert file"); + consulConfigOptions.addOption(null, CONSUL_CONFIG_KEY_PREFIX, true, "Prefix in the key-value store under which to store Exhibitor data, e.g. \"exhibitor/\""); + generalOptions = new Options(); generalOptions.addOption(null, TIMEOUT, true, "Connection timeout (ms) for ZK connections. Default is 30000."); generalOptions.addOption(null, LOGLINES, true, "Max lines of logging to keep in memory for display. Default is 1000."); @@ -167,7 +185,7 @@ public ExhibitorCLI() generalOptions.addOption(null, NODE_MUTATIONS, true, "If true, the Explorer UI will allow nodes to be modified (use with caution). Default is true."); generalOptions.addOption(null, JQUERY_STYLE, true, "Styling used for the JQuery-based UI. Currently available options: " + getStyleOptions()); generalOptions.addOption(ALT_HELP, HELP, false, "Print this help"); - generalOptions.addOption(SHORT_CONFIG_TYPE, CONFIG_TYPE, true, "Defines which configuration type you want to use. Choices are: \"file\", \"s3\", \"zookeeper\" or \"none\". Additional config will be required depending on which type you are using."); + generalOptions.addOption(SHORT_CONFIG_TYPE, CONFIG_TYPE, true, "Defines which configuration type you want to use. Choices are: \"file\", \"s3\", \"zookeeper\", \"consul\" or \"none\". Additional config will be required depending on which type you are using."); generalOptions.addOption(null, CONFIGCHECKMS, true, "Period (ms) to check for shared config updates. Default is: 30000"); generalOptions.addOption(null, SERVO_INTEGRATION, true, "true/false (default is false). If enabled, ZooKeeper will be queried once a minute for its state via the 'mntr' four letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish this data via JMX."); generalOptions.addOption(null, INITIAL_CONFIG_FILE, true, "Full path to a file that contains initial/default values for Exhibitor/ZooKeeper config values. The file is a standard property file. The property names are listed below. The file can specify some or all of the properties."); @@ -183,6 +201,7 @@ public ExhibitorCLI() addAll("Configuration Options for Type \"s3\"", s3ConfigOptions); addAll("Configuration Options for Type \"zookeeper\"", zookeeperConfigOptions); addAll("Configuration Options for Type \"file\"", fileConfigOptions); + addAll("Configuration Options for Type \"consul\"", consulConfigOptions); addAll("Configuration Options for Type \"none\"", noneConfigOptions); addAll("Backup Options", backupOptions); addAll("Authorization Options", authOptions); diff --git a/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCreator.java b/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCreator.java index da90e1da..5bbb6868 100644 --- a/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCreator.java +++ b/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/ExhibitorCreator.java @@ -19,6 +19,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.common.net.HostAndPort; import com.netflix.exhibitor.core.ExhibitorArguments; import com.netflix.exhibitor.core.backup.BackupProvider; import com.netflix.exhibitor.core.backup.filesystem.FileSystemBackupProvider; @@ -30,6 +31,7 @@ import com.netflix.exhibitor.core.config.JQueryStyle; import com.netflix.exhibitor.core.config.PropertyBasedInstanceConfig; import com.netflix.exhibitor.core.config.StringConfigs; +import com.netflix.exhibitor.core.config.consul.ConsulConfigProvider; import com.netflix.exhibitor.core.config.filesystem.FileSystemConfigProvider; import com.netflix.exhibitor.core.config.none.NoneConfigProvider; import com.netflix.exhibitor.core.config.s3.S3ConfigArguments; @@ -41,6 +43,7 @@ import com.netflix.exhibitor.core.s3.S3ClientFactoryImpl; import com.netflix.exhibitor.core.servo.ServoRegistration; import com.netflix.servo.jmx.JmxMonitorRegistry; +import com.orbitz.consul.Consul; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.ParseException; @@ -66,12 +69,20 @@ import org.mortbay.jetty.security.SecurityHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.Collections; import java.util.List; import java.util.Properties; @@ -295,6 +306,10 @@ else if ( configType.equals("zookeeper") ) { configProvider = getZookeeperProvider(commandLine, useHostname, defaultProperties); } + else if ( configType.equals("consul") ) + { + configProvider = getConsulProvider(cli, commandLine, defaultProperties); + } else if ( configType.equals("none") ) { log.warn("Warning: you have intentionally turned off shared configuration. This mode is meant for special purposes only. Please verify that this is your intent."); @@ -528,6 +543,54 @@ private ConfigProvider getS3Provider(ExhibitorCLI cli, CommandLine commandLine, return new S3ConfigProvider(new S3ClientFactoryImpl(), awsCredentials, awsClientConfig, getS3Arguments(cli, commandLine.getOptionValue(S3_CONFIG), prefix), hostname, defaultProperties, s3Region); } + private ConfigProvider getConsulProvider(ExhibitorCLI cli, CommandLine commandLine, Properties defaultProperties) throws Exception { + String host = commandLine.getOptionValue(CONSUL_CONFIG_HOST, "localhost"); + Integer port = Integer.valueOf(commandLine.getOptionValue(CONSUL_CONFIG_PORT, "8500")); + String prefix = commandLine.getOptionValue(CONSUL_CONFIG_KEY_PREFIX, "exhibitor/"); + + Boolean sslEnabled = Boolean.valueOf(commandLine.getOptionValue(CONSUL_CONFIG_SSL, "false")); + String protocol = sslEnabled ? "https" : "http"; + String url = String.format("%s://%s:%d", protocol, host, port); + + Consul.Builder builder = Consul.builder().withUrl(url); + if (sslEnabled) { + if (!Boolean.valueOf(commandLine.getOptionValue(CONSUL_CONFIG_SSL_VERIFY_HOSTNAME, "true"))) { + builder.withHostnameVerifier(new NullHostnameVerifier()); + } + + String sslProtocol = commandLine.getOptionValue(CONSUL_CONFIG_SSL_PROTOCOL, "TLSv1.2"); + String caCertPath = commandLine.getOptionValue(CONSUL_CONFIG_SSL_CA_CERT); + log.debug("SSL enabled for consul connections; protocol = %s, cacert = %s", + sslProtocol, caCertPath); + + // Load cacert file + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cacert = (X509Certificate) cf.generateCertificate(new FileInputStream(caCertPath)); + + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(null); + trustStore.setCertificateEntry("caCert", cacert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(trustStore); + + SSLContext sslContext = SSLContext.getInstance(sslProtocol); + sslContext.init(null, tmf.getTrustManagers(), null); + + builder.withSslContext(sslContext); + } + else { + log.debug("SSL is disabled for consul connections"); + } + + if (commandLine.hasOption(CONSUL_CONFIG_ACL_TOKEN)) { + builder.withAclToken(commandLine.getOptionValue(CONSUL_CONFIG_ACL_TOKEN)); + } + + Consul consul = builder.build(); + return new ConsulConfigProvider(consul, prefix, defaultProperties); + } + private void checkMutuallyExclusive(ExhibitorCLI cli, CommandLine commandLine, String option1, String option2) throws ExhibitorCreatorExit { if ( commandLine.hasOption(option1) && commandLine.hasOption(option2) ) diff --git a/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/NullHostnameVerifier.java b/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/NullHostnameVerifier.java new file mode 100644 index 00000000..6639fa31 --- /dev/null +++ b/exhibitor-standalone/src/main/java/com/netflix/exhibitor/standalone/NullHostnameVerifier.java @@ -0,0 +1,11 @@ +package com.netflix.exhibitor.standalone; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +public class NullHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } +} diff --git a/pom.xml b/pom.xml index 9b25d0d1..d38425f1 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,8 @@ 1.8.5 3.4.5 1.7.0 + 0.14.0 + 0.3.1 @@ -200,6 +202,18 @@ mockito-core ${mockito-version} + + + com.orbitz.consul + consul-client + ${consul-version} + + + + com.pszymczyk.consul + embedded-consul + ${embedded-consul-version} + From 6d3e3c045cd40094c1debdc2d73b81eb6bec4b1b Mon Sep 17 00:00:00 2001 From: thesquelched Date: Wed, 12 Apr 2017 13:39:55 -0500 Subject: [PATCH 2/2] Reap consul sessions when releasing lock; add session TTL --- .../core/config/consul/ConsulKvLock.java | 52 +++++++++++++++---- .../consul/TestConsulConfigProvider.java | 5 ++ 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java index 912eeda9..62548818 100644 --- a/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java +++ b/exhibitor-core/src/main/java/com/netflix/exhibitor/core/config/consul/ConsulKvLock.java @@ -14,26 +14,48 @@ public class ConsulKvLock { private final Consul consul; private final String path; private final String name; + private final String ttl; + private String sessionId; /** * @param consul consul client instance for connecting to consul cluster * @param path consul key-value path to lock * @param name a descriptive name for the lock + * @param ttl TTL, in seconds, for the consul session underpinning the lock */ - public ConsulKvLock(Consul consul, String path, String name) { + public ConsulKvLock(Consul consul, String path, String name, Integer ttl) { this.consul = consul; this.path = path; this.name = name; + this.ttl = ttl != null ? String.format("%ds", ttl) : null; + this.sessionId = null; + } + + /** + * @param consul consul client instance for connecting to consul cluster + * @param path consul key-value path to lock + * @param name a descriptive name for the lock + */ + public ConsulKvLock(Consul consul, String path, String name) { + this(consul, path, name, 60); } private String createSession() { - final ImmutableSession session = ImmutableSession.builder().name(name).build(); + final ImmutableSession session = ImmutableSession.builder() + .name(name) + .ttl(Optional.fromNullable(ttl)) + .build(); return consul.sessionClient().createSession(session).getId(); } + private void destroySession() { + consul.sessionClient().destroySession(sessionId); + sessionId = null; + } + public boolean acquireLock(long maxWait, TimeUnit unit) { KeyValueClient kv = consul.keyValueClient(); - String sessionId = createSession(); + sessionId = createSession(); Optional value = kv.getValue(path); @@ -43,18 +65,28 @@ public boolean acquireLock(long maxWait, TimeUnit unit) { BigInteger index = BigInteger.valueOf(value.get().getModifyIndex()); kv.getValue(path, QueryOptions.blockMinutes((int) unit.toMinutes(maxWait), index).build()); - return kv.acquireLock(path, sessionId); + + if (!kv.acquireLock(path, sessionId)) { + destroySession(); + return false; + } else { + return true; + } } public void releaseLock() { - KeyValueClient kv = consul.keyValueClient(); - Optional value = kv.getValue(path); + try { + KeyValueClient kv = consul.keyValueClient(); + Optional value = kv.getValue(path); - if (value.isPresent()) { - Optional session = value.get().getSession(); - if (session.isPresent()) { - kv.releaseLock(path, session.get()); + if (value.isPresent()) { + Optional session = value.get().getSession(); + if (session.isPresent()) { + kv.releaseLock(path, session.get()); + } } + } finally { + destroySession(); } } } diff --git a/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java b/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java index 3eaa0697..8bb8d8c3 100644 --- a/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java +++ b/exhibitor-core/src/test/java/com/netflix/exhibitor/core/config/consul/TestConsulConfigProvider.java @@ -5,6 +5,7 @@ import com.netflix.exhibitor.core.config.PropertyBasedInstanceConfig; import com.netflix.exhibitor.core.config.StringConfigs; import com.orbitz.consul.Consul; +import com.orbitz.consul.model.session.SessionInfo; import com.pszymczyk.consul.ConsulProcess; import com.pszymczyk.consul.ConsulStarterBuilder; import org.apache.curator.test.Timing; @@ -12,6 +13,7 @@ import org.testng.Assert; import org.testng.annotations.*; +import java.util.List; import java.util.Properties; @@ -57,6 +59,9 @@ public void testBasic() throws Exception { LoadedInstanceConfig instanceConfig = config.loadConfig(); Assert.assertEquals(instanceConfig.getConfig().getRootConfig().getString(StringConfigs.ZOO_CFG_EXTRA), "1,2,3"); + + List sessions = client.sessionClient().listSessions(); + Assert.assertEquals(sessions.size(), 0, "Consul session still exists!"); } finally { CloseableUtils.closeQuietly(config);