Add an experiment enrollment manager.
This commit is contained in:
parent
92f6a79e1f
commit
35fc98a188
|
@ -1,5 +1,19 @@
|
||||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class DynamicConfiguration {
|
public class DynamicConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Valid
|
||||||
|
private Map<String, DynamicExperimentEnrollmentConfiguration> experiments = Collections.emptyMap();
|
||||||
|
|
||||||
|
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(final String experimentName) {
|
||||||
|
return Optional.ofNullable(experiments.get(experimentName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class DynamicExperimentEnrollmentConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Valid
|
||||||
|
private Set<UUID> enrolledUuids = Collections.emptySet();
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Valid
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
private int enrollmentPercentage = 0;
|
||||||
|
|
||||||
|
public Set<UUID> getEnrolledUuids() {
|
||||||
|
return enrolledUuids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEnrollmentPercentage() {
|
||||||
|
return enrollmentPercentage;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.experiment;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicExperimentEnrollmentConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ExperimentEnrollmentManager {
|
||||||
|
|
||||||
|
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||||
|
|
||||||
|
public ExperimentEnrollmentManager(final DynamicConfigurationManager dynamicConfigurationManager) {
|
||||||
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnrolled(final Account account, final String experimentName) {
|
||||||
|
final Optional<DynamicExperimentEnrollmentConfiguration> maybeConfiguration = dynamicConfigurationManager.getConfiguration().getExperimentEnrollmentConfiguration(experimentName);
|
||||||
|
|
||||||
|
final Set<UUID> enrolledUuids = maybeConfiguration.map(DynamicExperimentEnrollmentConfiguration::getEnrolledUuids)
|
||||||
|
.orElse(Collections.emptySet());
|
||||||
|
|
||||||
|
final boolean enrolled;
|
||||||
|
|
||||||
|
if (enrolledUuids.contains(account.getUuid())) {
|
||||||
|
enrolled = true;
|
||||||
|
} else {
|
||||||
|
final int threshold = maybeConfiguration.map(DynamicExperimentEnrollmentConfiguration::getEnrollmentPercentage).orElse(0);
|
||||||
|
final int enrollmentHash = ((account.getUuid().hashCode() ^ experimentName.hashCode()) & Integer.MAX_VALUE) % 100;
|
||||||
|
|
||||||
|
enrolled = enrollmentHash < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
return enrolled;
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,6 @@ public class DynamicConfigurationManager implements Managed {
|
||||||
private final String clientId;
|
private final String clientId;
|
||||||
private final AmazonAppConfig appConfigClient;
|
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 AtomicReference<DynamicConfiguration> configuration = new AtomicReference<>();
|
||||||
private final AtomicBoolean running = new AtomicBoolean(true);
|
private final AtomicBoolean running = new AtomicBoolean(true);
|
||||||
private final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class);
|
private final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class);
|
||||||
|
@ -40,6 +39,8 @@ public class DynamicConfigurationManager implements Managed {
|
||||||
|
|
||||||
private boolean initialized = false;
|
private boolean initialized = false;
|
||||||
|
|
||||||
|
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
public DynamicConfigurationManager(String application, String environment, String configurationName) {
|
public DynamicConfigurationManager(String application, String environment, String configurationName) {
|
||||||
this(AmazonAppConfigClient.builder()
|
this(AmazonAppConfigClient.builder()
|
||||||
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(10000).withRequestTimeout(10000))
|
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(10000).withRequestTimeout(10000))
|
||||||
|
@ -104,7 +105,7 @@ public class DynamicConfigurationManager implements Managed {
|
||||||
|
|
||||||
if (!StringUtils.equals(lastConfigResult.getConfigurationVersion(), previousVersion)) {
|
if (!StringUtils.equals(lastConfigResult.getConfigurationVersion(), previousVersion)) {
|
||||||
logger.info("Received new config version: {}", lastConfigResult.getConfigurationVersion());
|
logger.info("Received new config version: {}", lastConfigResult.getConfigurationVersion());
|
||||||
maybeDynamicConfiguration = Optional.of(mapper.readValue(StandardCharsets.UTF_8.decode(lastConfigResult.getContent().asReadOnlyBuffer()).toString(), DynamicConfiguration.class));
|
maybeDynamicConfiguration = Optional.of(OBJECT_MAPPER.readValue(StandardCharsets.UTF_8.decode(lastConfigResult.getContent().asReadOnlyBuffer()).toString(), DynamicConfiguration.class));
|
||||||
} else {
|
} else {
|
||||||
// No change since last version
|
// No change since last version
|
||||||
maybeDynamicConfiguration = Optional.empty();
|
maybeDynamicConfiguration = Optional.empty();
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class DynamicConfigurationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseExperimentConfig() throws JsonProcessingException {
|
||||||
|
{
|
||||||
|
final String emptyConfigYaml = "test: true";
|
||||||
|
final DynamicConfiguration emptyConfig = DynamicConfigurationManager.OBJECT_MAPPER.readValue(emptyConfigYaml, DynamicConfiguration.class);
|
||||||
|
|
||||||
|
assertFalse(emptyConfig.getExperimentEnrollmentConfiguration("test").isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
final String experimentConfigYaml =
|
||||||
|
"experiments:\n" +
|
||||||
|
" percentageOnly:\n" +
|
||||||
|
" enrollmentPercentage: 12\n" +
|
||||||
|
" uuidsAndPercentage:\n" +
|
||||||
|
" enrolledUuids:\n" +
|
||||||
|
" - 717b1c09-ed0b-4120-bb0e-f4697534b8e1\n" +
|
||||||
|
" - 279f264c-56d7-4bbf-b9da-de718ff90903\n" +
|
||||||
|
" enrollmentPercentage: 77\n" +
|
||||||
|
" uuidsOnly:\n" +
|
||||||
|
" enrolledUuids:\n" +
|
||||||
|
" - 71618739-114c-4b1f-bb0d-6478a44eb600";
|
||||||
|
|
||||||
|
final DynamicConfiguration config = DynamicConfigurationManager.OBJECT_MAPPER.readValue(experimentConfigYaml, DynamicConfiguration.class);
|
||||||
|
|
||||||
|
assertFalse(config.getExperimentEnrollmentConfiguration("unconfigured").isPresent());
|
||||||
|
|
||||||
|
assertTrue(config.getExperimentEnrollmentConfiguration("percentageOnly").isPresent());
|
||||||
|
assertEquals(12, config.getExperimentEnrollmentConfiguration("percentageOnly").get().getEnrollmentPercentage());
|
||||||
|
assertEquals(Collections.emptySet(), config.getExperimentEnrollmentConfiguration("percentageOnly").get().getEnrolledUuids());
|
||||||
|
|
||||||
|
assertTrue(config.getExperimentEnrollmentConfiguration("uuidsAndPercentage").isPresent());
|
||||||
|
assertEquals(77, config.getExperimentEnrollmentConfiguration("uuidsAndPercentage").get().getEnrollmentPercentage());
|
||||||
|
assertEquals(Set.of(UUID.fromString("717b1c09-ed0b-4120-bb0e-f4697534b8e1"), UUID.fromString("279f264c-56d7-4bbf-b9da-de718ff90903")),
|
||||||
|
config.getExperimentEnrollmentConfiguration("uuidsAndPercentage").get().getEnrolledUuids());
|
||||||
|
|
||||||
|
assertTrue(config.getExperimentEnrollmentConfiguration("uuidsOnly").isPresent());
|
||||||
|
assertEquals(0, config.getExperimentEnrollmentConfiguration("uuidsOnly").get().getEnrollmentPercentage());
|
||||||
|
assertEquals(Set.of(UUID.fromString("71618739-114c-4b1f-bb0d-6478a44eb600")),
|
||||||
|
config.getExperimentEnrollmentConfiguration("uuidsOnly").get().getEnrolledUuids());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.experiment;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicExperimentEnrollmentConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ExperimentEnrollmentManagerTest {
|
||||||
|
|
||||||
|
private DynamicExperimentEnrollmentConfiguration experimentEnrollmentConfiguration;
|
||||||
|
|
||||||
|
private ExperimentEnrollmentManager experimentEnrollmentManager;
|
||||||
|
|
||||||
|
private Account account;
|
||||||
|
|
||||||
|
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
|
||||||
|
private static final String EXPERIMENT_NAME = "test";
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||||
|
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||||
|
|
||||||
|
experimentEnrollmentConfiguration = mock(DynamicExperimentEnrollmentConfiguration.class);
|
||||||
|
experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
|
||||||
|
account = mock(Account.class);
|
||||||
|
|
||||||
|
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||||
|
when(dynamicConfiguration.getExperimentEnrollmentConfiguration(EXPERIMENT_NAME)).thenReturn(Optional.of(experimentEnrollmentConfiguration));
|
||||||
|
when(account.getUuid()).thenReturn(ACCOUNT_UUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsEnrolled() {
|
||||||
|
assertFalse(experimentEnrollmentManager.isEnrolled(account, EXPERIMENT_NAME));
|
||||||
|
assertFalse(experimentEnrollmentManager.isEnrolled(account, EXPERIMENT_NAME + "-unrelated-experiment"));
|
||||||
|
|
||||||
|
when(experimentEnrollmentConfiguration.getEnrolledUuids()).thenReturn(Set.of(ACCOUNT_UUID));
|
||||||
|
assertTrue(experimentEnrollmentManager.isEnrolled(account, EXPERIMENT_NAME));
|
||||||
|
|
||||||
|
when(experimentEnrollmentConfiguration.getEnrolledUuids()).thenReturn(Collections.emptySet());
|
||||||
|
when(experimentEnrollmentConfiguration.getEnrollmentPercentage()).thenReturn(0);
|
||||||
|
|
||||||
|
assertFalse(experimentEnrollmentManager.isEnrolled(account, EXPERIMENT_NAME));
|
||||||
|
|
||||||
|
when(experimentEnrollmentConfiguration.getEnrollmentPercentage()).thenReturn(100);
|
||||||
|
assertTrue(experimentEnrollmentManager.isEnrolled(account, EXPERIMENT_NAME));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue