Add a dynamic configuration manager
This commit is contained in:
parent
5a9c8e304c
commit
92f6a79e1f
|
@ -83,12 +83,17 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.amazonaws</groupId>
|
<groupId>com.amazonaws</groupId>
|
||||||
<artifactId>aws-java-sdk-s3</artifactId>
|
<artifactId>aws-java-sdk-s3</artifactId>
|
||||||
<version>1.11.366</version>
|
<version>1.11.939</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.amazonaws</groupId>
|
<groupId>com.amazonaws</groupId>
|
||||||
<artifactId>aws-java-sdk-sqs</artifactId>
|
<artifactId>aws-java-sdk-sqs</artifactId>
|
||||||
<version>1.11.366</version>
|
<version>1.11.939</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.amazonaws</groupId>
|
||||||
|
<artifactId>aws-java-sdk-appconfig</artifactId>
|
||||||
|
<version>1.11.939</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import io.dropwizard.Configuration;
|
||||||
import io.dropwizard.client.JerseyClientConfiguration;
|
import io.dropwizard.client.JerseyClientConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
||||||
|
@ -216,6 +217,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RemoteConfigConfiguration remoteConfig;
|
private RemoteConfigConfiguration remoteConfig;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private AppConfigConfiguration appConfig;
|
||||||
|
|
||||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||||
|
|
||||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||||
|
@ -371,4 +377,8 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
|
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
|
||||||
return remoteConfig;
|
return remoteConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppConfigConfiguration getAppConfig() {
|
||||||
|
return appConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ import org.whispersystems.textsecuregcm.storage.ActiveUserCounter;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
|
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
|
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||||
import org.whispersystems.textsecuregcm.storage.FeatureFlags;
|
import org.whispersystems.textsecuregcm.storage.FeatureFlags;
|
||||||
import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager;
|
import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager;
|
||||||
|
@ -328,6 +329,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheCluster);
|
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheCluster);
|
||||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||||
|
|
||||||
|
DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager(config.getAppConfig().getApplication(), config.getAppConfig().getEnvironment(), config.getAppConfig().getConfigurationName());
|
||||||
|
|
||||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||||
|
|
||||||
|
@ -373,6 +376,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
environment.lifecycle().manage(messagePersister);
|
environment.lifecycle().manage(messagePersister);
|
||||||
environment.lifecycle().manage(clientPresenceManager);
|
environment.lifecycle().manage(clientPresenceManager);
|
||||||
environment.lifecycle().manage(featureFlagsManager);
|
environment.lifecycle().manage(featureFlagsManager);
|
||||||
|
environment.lifecycle().manage(dynamicConfigurationManager);
|
||||||
|
|
||||||
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
||||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
|
public class AppConfigConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String application;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String environment;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String configuration;
|
||||||
|
|
||||||
|
public String getApplication() {
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEnvironment() {
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfigurationName() {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||||
|
|
||||||
|
public class DynamicConfiguration {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import com.amazonaws.ClientConfiguration;
|
||||||
|
import com.amazonaws.services.appconfig.AmazonAppConfig;
|
||||||
|
import com.amazonaws.services.appconfig.AmazonAppConfigClient;
|
||||||
|
import com.amazonaws.services.appconfig.model.GetConfigurationRequest;
|
||||||
|
import com.amazonaws.services.appconfig.model.GetConfigurationResult;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public class DynamicConfigurationManager implements Managed {
|
||||||
|
|
||||||
|
private final String application;
|
||||||
|
private final String environment;
|
||||||
|
private final String configurationName;
|
||||||
|
private final String clientId;
|
||||||
|
private final AmazonAppConfig appConfigClient;
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
private final AtomicReference<DynamicConfiguration> configuration = new AtomicReference<>();
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(true);
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class);
|
||||||
|
|
||||||
|
private GetConfigurationResult lastConfigResult;
|
||||||
|
|
||||||
|
private boolean initialized = false;
|
||||||
|
|
||||||
|
public DynamicConfigurationManager(String application, String environment, String configurationName) {
|
||||||
|
this(AmazonAppConfigClient.builder()
|
||||||
|
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(10000).withRequestTimeout(10000))
|
||||||
|
.build(),
|
||||||
|
application, environment, configurationName, UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public DynamicConfigurationManager(AmazonAppConfig appConfigClient, String application, String environment, String configurationName, String clientId) {
|
||||||
|
this.appConfigClient = appConfigClient;
|
||||||
|
this.application = application;
|
||||||
|
this.environment = environment;
|
||||||
|
this.configurationName = configurationName;
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DynamicConfiguration getConfiguration() {
|
||||||
|
synchronized (this) {
|
||||||
|
while (!initialized) Util.wait(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return configuration.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
configuration.set(retrieveInitialDynamicConfiguration());
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
this.initialized = true;
|
||||||
|
this.notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
while (running.get()) {
|
||||||
|
try {
|
||||||
|
retrieveDynamicConfiguration().ifPresent(configuration::set);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Error retrieving dynamic configuration", t);
|
||||||
|
}
|
||||||
|
|
||||||
|
Util.sleep(5000);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<DynamicConfiguration> retrieveDynamicConfiguration() throws JsonProcessingException {
|
||||||
|
final String previousVersion = lastConfigResult != null ? lastConfigResult.getConfigurationVersion() : null;
|
||||||
|
|
||||||
|
lastConfigResult = appConfigClient.getConfiguration(new GetConfigurationRequest().withApplication(application)
|
||||||
|
.withEnvironment(environment)
|
||||||
|
.withConfiguration(configurationName)
|
||||||
|
.withClientId(clientId)
|
||||||
|
.withClientConfigurationVersion(previousVersion));
|
||||||
|
|
||||||
|
final Optional<DynamicConfiguration> maybeDynamicConfiguration;
|
||||||
|
|
||||||
|
if (!StringUtils.equals(lastConfigResult.getConfigurationVersion(), previousVersion)) {
|
||||||
|
logger.info("Received new config version: {}", lastConfigResult.getConfigurationVersion());
|
||||||
|
maybeDynamicConfiguration = Optional.of(mapper.readValue(StandardCharsets.UTF_8.decode(lastConfigResult.getContent().asReadOnlyBuffer()).toString(), DynamicConfiguration.class));
|
||||||
|
} else {
|
||||||
|
// No change since last version
|
||||||
|
maybeDynamicConfiguration = Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return maybeDynamicConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DynamicConfiguration retrieveInitialDynamicConfiguration() {
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
final Optional<DynamicConfiguration> maybeDynamicConfiguration = retrieveDynamicConfiguration();
|
||||||
|
|
||||||
|
if (maybeDynamicConfiguration.isPresent()) {
|
||||||
|
return maybeDynamicConfiguration.get();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("No initial configuration available");
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Error retrieving initial dynamic configuration", t);
|
||||||
|
Util.sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import com.amazonaws.services.appconfig.AmazonAppConfig;
|
||||||
|
import com.amazonaws.services.appconfig.model.GetConfigurationRequest;
|
||||||
|
import com.amazonaws.services.appconfig.model.GetConfigurationResult;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class DynamicConfigurationManagerTest {
|
||||||
|
|
||||||
|
private DynamicConfigurationManager dynamicConfigurationManager;
|
||||||
|
private AmazonAppConfig appConfig;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
this.appConfig = mock(AmazonAppConfig.class);
|
||||||
|
this.dynamicConfigurationManager = new DynamicConfigurationManager(appConfig, "foo", "bar", "baz", "poof");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetConfig() {
|
||||||
|
ArgumentCaptor<GetConfigurationRequest> captor = ArgumentCaptor.forClass(GetConfigurationRequest.class);
|
||||||
|
when(appConfig.getConfiguration(captor.capture())).thenReturn(new GetConfigurationResult().withContent(ByteBuffer.wrap("test: true".getBytes()))
|
||||||
|
.withConfigurationVersion("1"));
|
||||||
|
|
||||||
|
dynamicConfigurationManager.start();
|
||||||
|
|
||||||
|
assertThat(captor.getValue().getApplication()).isEqualTo("foo");
|
||||||
|
assertThat(captor.getValue().getEnvironment()).isEqualTo("bar");
|
||||||
|
assertThat(captor.getValue().getConfiguration()).isEqualTo("baz");
|
||||||
|
assertThat(captor.getValue().getClientId()).isEqualTo("poof");
|
||||||
|
|
||||||
|
assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue