Add a dynamic configuration manager
This commit is contained in:
parent
5a9c8e304c
commit
92f6a79e1f
|
@ -83,12 +83,17 @@
|
|||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
<version>1.11.366</version>
|
||||
<version>1.11.939</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<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>
|
||||
|
|
|
@ -9,6 +9,7 @@ import io.dropwizard.Configuration;
|
|||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
||||
|
@ -216,6 +217,11 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private RemoteConfigConfiguration remoteConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private AppConfigConfiguration appConfig;
|
||||
|
||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||
|
||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||
|
@ -371,4 +377,8 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
|
||||
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.DirectoryReconciler;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||
import org.whispersystems.textsecuregcm.storage.FeatureFlags;
|
||||
import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager;
|
||||
|
@ -328,6 +329,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheCluster);
|
||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||
|
||||
DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager(config.getAppConfig().getApplication(), config.getAppConfig().getEnvironment(), config.getAppConfig().getConfigurationName());
|
||||
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
|
||||
|
@ -373,6 +376,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
environment.lifecycle().manage(messagePersister);
|
||||
environment.lifecycle().manage(clientPresenceManager);
|
||||
environment.lifecycle().manage(featureFlagsManager);
|
||||
environment.lifecycle().manage(dynamicConfigurationManager);
|
||||
|
||||
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
||||
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