From ff448950ed19a02ecc18fe9247ce45f839f5a4a0 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 12 Feb 2021 11:52:40 -0500 Subject: [PATCH] Collapse the feature flag system into the dynamic config system. --- .../textsecuregcm/WhisperServerService.java | 13 +- .../dynamic/DynamicConfiguration.java | 8 + .../textsecuregcm/storage/FeatureFlags.java | 82 --------- .../storage/FeatureFlagsManager.java | 102 ----------- .../storage/MessagePersister.java | 21 ++- .../workers/AbstractFeatureFlagTask.java | 31 ---- .../workers/DeleteFeatureFlagTask.java | 35 ---- .../workers/ListFeatureFlagsTask.java | 24 --- .../workers/SetFeatureFlagTask.java | 40 ----- .../textsecuregcm/workers/VacuumCommand.java | 5 - .../dynamic/DynamicConfigurationTest.java | 165 +++++++++++------- .../storage/FeatureFlagsManagerTest.java | 51 ------ .../storage/FeatureFlagsTest.java | 68 -------- .../MessagePersisterIntegrationTest.java | 5 +- .../storage/MessagePersisterTest.java | 7 +- .../workers/DeleteFeatureFlagTaskTest.java | 40 ----- .../workers/SetFeatureFlagTaskTest.java | 40 ----- 17 files changed, 127 insertions(+), 610 deletions(-) delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlags.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManager.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractFeatureFlagTask.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTask.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/ListFeatureFlagsTask.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTask.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManagerTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTaskTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTaskTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index f1bceb8b1..fa7f5a0a7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -139,8 +139,6 @@ 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; import org.whispersystems.textsecuregcm.storage.KeysDynamoDb; import org.whispersystems.textsecuregcm.storage.MessagePersister; import org.whispersystems.textsecuregcm.storage.MessagesCache; @@ -166,13 +164,10 @@ import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler; import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener; import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; import org.whispersystems.textsecuregcm.workers.CertificateCommand; -import org.whispersystems.textsecuregcm.workers.DeleteFeatureFlagTask; import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; import org.whispersystems.textsecuregcm.workers.GetRedisCommandStatsCommand; import org.whispersystems.textsecuregcm.workers.GetRedisSlowlogCommand; -import org.whispersystems.textsecuregcm.workers.ListFeatureFlagsTask; import org.whispersystems.textsecuregcm.workers.SetCrawlerAccelerationTask; -import org.whispersystems.textsecuregcm.workers.SetFeatureFlagTask; import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask; import org.whispersystems.textsecuregcm.workers.VacuumCommand; import org.whispersystems.textsecuregcm.workers.ZkParamsCommand; @@ -284,7 +279,6 @@ public class WhisperServerService extends Application accountDatabaseCrawlerListeners = new ArrayList<>(); accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager, directoryQueue)); @@ -383,7 +376,6 @@ public class WhisperServerService extends Application featureFlags = Collections.emptySet(); + public Optional getExperimentEnrollmentConfiguration(final String experimentName) { return Optional.ofNullable(experiments.get(experimentName)); } @@ -32,4 +36,8 @@ public class DynamicConfiguration { public DynamicRemoteDeprecationConfiguration getRemoteDeprecationConfiguration() { return remoteDeprecation; } + + public Set getActiveFeatureFlags() { + return featureFlags; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlags.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlags.java deleted file mode 100644 index f54183b79..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlags.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.storage; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.SharedMetricRegistries; -import com.codahale.metrics.Timer; -import org.jdbi.v3.core.mapper.RowMapper; -import org.whispersystems.textsecuregcm.util.Constants; -import org.whispersystems.textsecuregcm.util.Pair; - -import java.util.Map; -import java.util.stream.Collectors; - -import static com.codahale.metrics.MetricRegistry.name; - -/** - * The feature flag database is a persistent store of the state of all server-side feature flags. Feature flags are - * identified by a human-readable name (e.g. "invert-nano-flappers") and are either active or inactive. - *

- * The feature flag database provides the most up-to-date possible view of feature flags, but does so at the cost of - * interacting with a remote data store. In nearly all cases, callers should prefer a cached, eventually-consistent - * view of feature flags (see {@link FeatureFlagsManager}). - *

- * When an operation requiring a feature flag has finished, callers should delete the feature flag to prevent - * accumulation of non-functional flags. - */ -public class FeatureFlags { - - private final FaultTolerantDatabase database; - - private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); - private final Timer getAllTimer = metricRegistry.timer(name(getClass(), "getAll")); - private final Timer updateTimer = metricRegistry.timer(name(getClass(), "update")); - private final Timer deleteTimer = metricRegistry.timer(name(getClass(), "delete")); - private final Timer vacuumTimer = metricRegistry.timer(name(getClass(), "vacuum")); - - private static final RowMapper> PAIR_ROW_MAPPER = (resultSet, statementContext) -> - new Pair<>(resultSet.getString("flag"), resultSet.getBoolean("active")); - - public FeatureFlags(final FaultTolerantDatabase database) { - this.database = database; - } - - public Map getFeatureFlags() { - try (final Timer.Context ignored = getAllTimer.time()) { - return database.with(jdbi -> jdbi.withHandle(handle -> handle.createQuery("SELECT flag, active FROM feature_flags") - .map(PAIR_ROW_MAPPER) - .list() - .stream() - .collect(Collectors.toMap(Pair::first, Pair::second)))); - } - } - - public void setFlag(final String featureFlag, final boolean active) { - try (final Timer.Context ignored = updateTimer.time()) { - database.use(jdbi -> jdbi.withHandle(handle -> handle.createUpdate("INSERT INTO feature_flags (flag, active) VALUES (:featureFlag, :active) ON CONFLICT (flag) DO UPDATE SET active = EXCLUDED.active") - .bind("featureFlag", featureFlag) - .bind("active", active) - .execute())); - } - } - - public void deleteFlag(final String featureFlag) { - try (final Timer.Context ignored = deleteTimer.time()) { - database.use(jdbi -> jdbi.withHandle(handle -> handle.createUpdate("DELETE FROM feature_flags WHERE flag = :featureFlag") - .bind("featureFlag", featureFlag) - .execute())); - } - } - - public void vacuum() { - try (final Timer.Context ignored = vacuumTimer.time()) { - database.use(jdbi -> jdbi.useHandle(handle -> { - handle.execute("VACUUM feature_flags"); - })); - } - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManager.java deleted file mode 100644 index aaa569e96..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManager.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.storage; - -import com.google.common.annotations.VisibleForTesting; -import io.dropwizard.lifecycle.Managed; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.Metrics; - -import java.time.Duration; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import static com.codahale.metrics.MetricRegistry.name; - -/** - * The feature flags manager provides a high-throughput, eventually-consistent view of feature flags. This is the main - * channel through which callers should interact with feature flags. - *

- * Feature flags are intended to provide temporary control over server-side features (i.e. for migrations or experiments - * with new services). Each flag is identified by a human-readable name (e.g. "invert-nano-flappers") and is either - * active or inactive. Flags (including flags that have not been set) are inactive by default. - */ -public class FeatureFlagsManager implements Managed { - - private final FeatureFlags featureFlagDatabase; - private final ScheduledExecutorService refreshExecutorService; - private ScheduledFuture refreshFuture; - private final AtomicReference> featureFlags = new AtomicReference<>(Collections.emptyMap()); - private final Set gauges = new HashSet<>(); - - private static final String GAUGE_NAME = "status"; - private static final String FLAG_TAG_NAME = "flag"; - - private static final Duration REFRESH_INTERVAL = Duration.ofSeconds(30); - - public FeatureFlagsManager(final FeatureFlags featureFlagDatabase, final ScheduledExecutorService refreshExecutorService) { - this.featureFlagDatabase = featureFlagDatabase; - this.refreshExecutorService = refreshExecutorService; - - refreshFeatureFlags(); - } - - @Override - public void start() { - refreshFuture = refreshExecutorService.scheduleAtFixedRate(this::refreshFeatureFlags, 0, REFRESH_INTERVAL.toSeconds(), TimeUnit.SECONDS); - } - - @Override - public void stop() { - refreshFuture.cancel(false); - } - - public boolean isFeatureFlagActive(final String featureFlag) { - return featureFlags.get().getOrDefault(featureFlag, false); - } - - public void setFeatureFlag(final String featureFlag, final boolean active) { - featureFlagDatabase.setFlag(featureFlag, active); - refreshFeatureFlags(); - } - - public void deleteFeatureFlag(final String featureFlag) { - featureFlagDatabase.deleteFlag(featureFlag); - refreshFeatureFlags(); - } - - public Map getAllFlags() { - return featureFlags.get(); - } - - @VisibleForTesting - void refreshFeatureFlags() { - final Map refreshedFeatureFlags = featureFlagDatabase.getFeatureFlags(); - - featureFlags.set(Collections.unmodifiableMap(refreshedFeatureFlags)); - - for (final Gauge gauge : gauges) { - Metrics.globalRegistry.remove(gauge); - } - - gauges.clear(); - - for (final Map.Entry entry : refreshedFeatureFlags.entrySet()) { - final String featureFlag = entry.getKey(); - final boolean active = entry.getValue(); - - gauges.add(Gauge.builder(name(getClass(), GAUGE_NAME), () -> active ? 1 : 0) - .tag(FLAG_TAG_NAME, featureFlag) - .register(Metrics.globalRegistry)); - } - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java index 52267c02b..d86a6eddb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java @@ -28,10 +28,9 @@ import static com.codahale.metrics.MetricRegistry.name; public class MessagePersister implements Managed { - private final MessagesCache messagesCache; - private final MessagesManager messagesManager; - private final AccountsManager accountsManager; - private final FeatureFlagsManager featureFlagsManager; + private final MessagesCache messagesCache; + private final MessagesManager messagesManager; + private final AccountsManager accountsManager; private final Duration persistDelay; @@ -49,21 +48,21 @@ public class MessagePersister implements Managed { static final int QUEUE_BATCH_LIMIT = 100; static final int MESSAGE_BATCH_LIMIT = 100; + private static final String DISABLE_PERSISTER_FEATURE_FLAG = "DISABLE_MESSAGE_PERSISTER"; private static final int WORKER_THREAD_COUNT = 4; private static final Logger logger = LoggerFactory.getLogger(MessagePersister.class); - public MessagePersister(final MessagesCache messagesCache, final MessagesManager messagesManager, final AccountsManager accountsManager, final FeatureFlagsManager featureFlagsManager, final Duration persistDelay) { - this.messagesCache = messagesCache; - this.messagesManager = messagesManager; - this.accountsManager = accountsManager; - this.featureFlagsManager = featureFlagsManager; - this.persistDelay = persistDelay; + public MessagePersister(final MessagesCache messagesCache, final MessagesManager messagesManager, final AccountsManager accountsManager, final DynamicConfigurationManager dynamicConfigurationManager, final Duration persistDelay) { + this.messagesCache = messagesCache; + this.messagesManager = messagesManager; + this.accountsManager = accountsManager; + this.persistDelay = persistDelay; for (int i = 0; i < workerThreads.length; i++) { workerThreads[i] = new Thread(() -> { while (running) { - if (featureFlagsManager.isFeatureFlagActive("DISABLE_MESSAGE_PERSISTER")) { + if (dynamicConfigurationManager.getConfiguration().getActiveFeatureFlags().contains(DISABLE_PERSISTER_FEATURE_FLAG)) { Util.sleep(1000); } else { try { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractFeatureFlagTask.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractFeatureFlagTask.java deleted file mode 100644 index 893dc6502..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractFeatureFlagTask.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import io.dropwizard.servlets.tasks.Task; -import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager; - -import java.io.PrintWriter; - -public abstract class AbstractFeatureFlagTask extends Task { - - private final FeatureFlagsManager featureFlagsManager; - - protected AbstractFeatureFlagTask(final String name, final FeatureFlagsManager featureFlagsManager) { - super(name); - - this.featureFlagsManager = featureFlagsManager; - } - - protected FeatureFlagsManager getFeatureFlagsManager() { - return featureFlagsManager; - } - - protected void printFeatureFlags(final PrintWriter out) { - out.println("Feature flags:"); - featureFlagsManager.getAllFlags().forEach((flag, active) -> out.println(flag + ": " + active)); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTask.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTask.java deleted file mode 100644 index 36f260444..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTask.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager; - -import java.io.PrintWriter; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class DeleteFeatureFlagTask extends AbstractFeatureFlagTask { - - public DeleteFeatureFlagTask(final FeatureFlagsManager featureFlagsManager) { - super("delete-feature-flag", featureFlagsManager); - } - - @Override - public void execute(final Map> parameters, final PrintWriter out) { - if (parameters.containsKey("flag")) { - for (final String flag : parameters.getOrDefault("flag", Collections.emptyList())) { - out.println("Deleting feature flag: " + flag); - getFeatureFlagsManager().deleteFeatureFlag(flag); - } - - out.println(); - printFeatureFlags(out); - } else { - out.println("Usage: delete-feature-flag?flag=FLAG_NAME[&flag=FLAG_NAME2&flag=...]"); - } - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ListFeatureFlagsTask.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ListFeatureFlagsTask.java deleted file mode 100644 index ef6fb254f..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ListFeatureFlagsTask.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager; - -import java.io.PrintWriter; -import java.util.List; -import java.util.Map; - -public class ListFeatureFlagsTask extends AbstractFeatureFlagTask { - - public ListFeatureFlagsTask(final FeatureFlagsManager featureFlagsManager) { - super("list-feature-flags", featureFlagsManager); - } - - @Override - public void execute(final Map> parameters, final PrintWriter out) { - printFeatureFlags(out); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTask.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTask.java deleted file mode 100644 index d55c5178c..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTask.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager; - -import java.io.PrintWriter; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public class SetFeatureFlagTask extends AbstractFeatureFlagTask { - - public SetFeatureFlagTask(final FeatureFlagsManager featureFlagsManager) { - super("set-feature-flag", featureFlagsManager); - } - - @Override - public void execute(final Map> parameters, final PrintWriter out) { - final Optional maybeFlag = Optional.ofNullable(parameters.get("flag")) - .flatMap(values -> values.stream().findFirst()); - - final Optional maybeActive = Optional.ofNullable(parameters.get("active")) - .flatMap(values -> values.stream().findFirst()) - .map(Boolean::valueOf); - - if (maybeFlag.isPresent() && maybeActive.isPresent()) { - getFeatureFlagsManager().setFeatureFlag(maybeFlag.get(), maybeActive.get()); - - out.format("Set %s to %s\n", maybeFlag.get(), maybeActive.get()); - out.println(); - printFeatureFlags(out); - } else { - out.println("Usage: set-feature-flag?flag=FLAG_NAME&active=[true|false]"); - } - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/VacuumCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/VacuumCommand.java index e75a21657..5b5b7071d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/VacuumCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/VacuumCommand.java @@ -13,7 +13,6 @@ import org.whispersystems.textsecuregcm.WhisperServerConfiguration; import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration; import org.whispersystems.textsecuregcm.storage.Accounts; import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase; -import org.whispersystems.textsecuregcm.storage.FeatureFlags; import org.whispersystems.textsecuregcm.storage.PendingAccounts; import io.dropwizard.cli.ConfiguredCommand; @@ -40,7 +39,6 @@ public class VacuumCommand extends ConfiguredCommand Accounts accounts = new Accounts(accountDatabase); PendingAccounts pendingAccounts = new PendingAccounts(accountDatabase); - FeatureFlags featureFlags = new FeatureFlags(accountDatabase); logger.info("Vacuuming accounts..."); accounts.vacuum(); @@ -48,9 +46,6 @@ public class VacuumCommand extends ConfiguredCommand logger.info("Vacuuming pending_accounts..."); pendingAccounts.vacuum(); - logger.info("Vacuuming feature flags..."); - featureFlags.vacuum(); - Thread.sleep(3000); System.exit(0); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java index ce2ed26a7..a80d7b18f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java @@ -20,79 +20,112 @@ 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); + @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()); - } + assertFalse(emptyConfig.getExperimentEnrollmentConfiguration("test").isPresent()); } - @Test - public void testParseRemoteDeprecationConfig() throws JsonProcessingException { - { - final String emptyConfigYaml = "test: true"; - final DynamicConfiguration emptyConfig = DynamicConfigurationManager.OBJECT_MAPPER.readValue(emptyConfigYaml, DynamicConfiguration.class); + { + 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"; - assertNotNull(emptyConfig.getRemoteDeprecationConfiguration()); - } + final DynamicConfiguration config = DynamicConfigurationManager.OBJECT_MAPPER + .readValue(experimentConfigYaml, DynamicConfiguration.class); - { - final String experimentConfigYaml = - "remoteDeprecation:\n" + - " minimumVersions:\n" + - " IOS: 1.2.3\n" + - " ANDROID: 4.5.6\n" + + assertFalse(config.getExperimentEnrollmentConfiguration("unconfigured").isPresent()); - " versionsPendingDeprecation:\n" + - " DESKTOP: 7.8.9\n" + + assertTrue(config.getExperimentEnrollmentConfiguration("percentageOnly").isPresent()); + assertEquals(12, config.getExperimentEnrollmentConfiguration("percentageOnly").get().getEnrollmentPercentage()); + assertEquals(Collections.emptySet(), + config.getExperimentEnrollmentConfiguration("percentageOnly").get().getEnrolledUuids()); - " blockedVersions:\n" + - " DESKTOP:\n" + - " - 1.4.0-beta.2"; + 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()); - final DynamicConfiguration config = DynamicConfigurationManager.OBJECT_MAPPER.readValue(experimentConfigYaml, DynamicConfiguration.class); - final DynamicRemoteDeprecationConfiguration remoteDeprecationConfiguration = config.getRemoteDeprecationConfiguration(); - - assertEquals(Map.of(ClientPlatform.IOS, new Semver("1.2.3"), ClientPlatform.ANDROID, new Semver("4.5.6")), remoteDeprecationConfiguration.getMinimumVersions()); - assertEquals(Map.of(ClientPlatform.DESKTOP, new Semver("7.8.9")), remoteDeprecationConfiguration.getVersionsPendingDeprecation()); - assertEquals(Map.of(ClientPlatform.DESKTOP, Set.of(new Semver("1.4.0-beta.2"))), remoteDeprecationConfiguration.getBlockedVersions()); - assertTrue(remoteDeprecationConfiguration.getVersionsPendingBlock().isEmpty()); - } + 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()); } + } + + @Test + public void testParseRemoteDeprecationConfig() throws JsonProcessingException { + { + final String emptyConfigYaml = "test: true"; + final DynamicConfiguration emptyConfig = DynamicConfigurationManager.OBJECT_MAPPER + .readValue(emptyConfigYaml, DynamicConfiguration.class); + + assertNotNull(emptyConfig.getRemoteDeprecationConfiguration()); + } + + { + final String experimentConfigYaml = + "remoteDeprecation:\n" + + " minimumVersions:\n" + + " IOS: 1.2.3\n" + + " ANDROID: 4.5.6\n" + + + " versionsPendingDeprecation:\n" + + " DESKTOP: 7.8.9\n" + + + " blockedVersions:\n" + + " DESKTOP:\n" + + " - 1.4.0-beta.2"; + + final DynamicConfiguration config = DynamicConfigurationManager.OBJECT_MAPPER + .readValue(experimentConfigYaml, DynamicConfiguration.class); + final DynamicRemoteDeprecationConfiguration remoteDeprecationConfiguration = config + .getRemoteDeprecationConfiguration(); + + assertEquals(Map.of(ClientPlatform.IOS, new Semver("1.2.3"), ClientPlatform.ANDROID, new Semver("4.5.6")), + remoteDeprecationConfiguration.getMinimumVersions()); + assertEquals(Map.of(ClientPlatform.DESKTOP, new Semver("7.8.9")), + remoteDeprecationConfiguration.getVersionsPendingDeprecation()); + assertEquals(Map.of(ClientPlatform.DESKTOP, Set.of(new Semver("1.4.0-beta.2"))), + remoteDeprecationConfiguration.getBlockedVersions()); + assertTrue(remoteDeprecationConfiguration.getVersionsPendingBlock().isEmpty()); + } + } + + @Test + public void testParseFeatureFlags() throws JsonProcessingException { + { + final String emptyConfigYaml = "test: true"; + final DynamicConfiguration emptyConfig = DynamicConfigurationManager.OBJECT_MAPPER + .readValue(emptyConfigYaml, DynamicConfiguration.class); + + assertTrue(emptyConfig.getActiveFeatureFlags().isEmpty()); + } + + { + final String emptyConfigYaml = + "featureFlags:\n" + + " - testFlag"; + + final DynamicConfiguration emptyConfig = DynamicConfigurationManager.OBJECT_MAPPER + .readValue(emptyConfigYaml, DynamicConfiguration.class); + + assertTrue(emptyConfig.getActiveFeatureFlags().contains("testFlag")); + } + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManagerTest.java deleted file mode 100644 index 898c7c4f8..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsManagerTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.storage; - -import com.opentable.db.postgres.embedded.LiquibasePreparer; -import com.opentable.db.postgres.junit.EmbeddedPostgresRules; -import com.opentable.db.postgres.junit.PreparedDbRule; -import org.jdbi.v3.core.Jdbi; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; - -import java.util.concurrent.ScheduledExecutorService; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -public class FeatureFlagsManagerTest { - - private FeatureFlagsManager featureFlagsManager; - - @Rule - public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml")); - - @Before - public void setUp() { - final FaultTolerantDatabase database = new FaultTolerantDatabase("featureFlagsTest", - Jdbi.create(db.getTestDatabase()), - new CircuitBreakerConfiguration()); - - featureFlagsManager = new FeatureFlagsManager(new FeatureFlags(database), mock(ScheduledExecutorService.class)); - } - - @Test - public void testIsFeatureFlagActive() { - final String flagName = "testFlag"; - - assertFalse(featureFlagsManager.isFeatureFlagActive(flagName)); - - featureFlagsManager.setFeatureFlag(flagName, true); - assertTrue(featureFlagsManager.isFeatureFlagActive(flagName)); - - featureFlagsManager.setFeatureFlag(flagName, false); - assertFalse(featureFlagsManager.isFeatureFlagActive(flagName)); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsTest.java deleted file mode 100644 index efd7afe7a..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FeatureFlagsTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.storage; - -import com.opentable.db.postgres.embedded.LiquibasePreparer; -import com.opentable.db.postgres.junit.EmbeddedPostgresRules; -import com.opentable.db.postgres.junit.PreparedDbRule; -import org.jdbi.v3.core.Jdbi; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; - -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class FeatureFlagsTest { - - @Rule - public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml")); - - private FeatureFlags featureFlags; - - @Before - public void setUp() { - final FaultTolerantDatabase database = new FaultTolerantDatabase("featureFlagsTest", - Jdbi.create(db.getTestDatabase()), - new CircuitBreakerConfiguration()); - - this.featureFlags = new FeatureFlags(database); - } - - @Test - public void testSetFlagIsFlagActive() { - assertTrue(featureFlags.getFeatureFlags().isEmpty()); - - featureFlags.setFlag("testFlag", true); - assertEquals(Map.of("testFlag", true), featureFlags.getFeatureFlags()); - - featureFlags.setFlag("testFlag", false); - assertEquals(Map.of("testFlag", false), featureFlags.getFeatureFlags()); - } - - @Test - public void testDeleteFlag() { - assertTrue(featureFlags.getFeatureFlags().isEmpty()); - - featureFlags.setFlag("testFlag", true); - assertEquals(Map.of("testFlag", true), featureFlags.getFeatureFlags()); - - featureFlags.deleteFlag("testFlag"); - assertTrue(featureFlags.getFeatureFlags().isEmpty()); - } - - @Test - public void testVacuum() { - featureFlags.setFlag("testFlag", true); - assertEquals(Map.of("testFlag", true), featureFlags.getFeatureFlags()); - - featureFlags.vacuum(); - assertEquals(Map.of("testFlag", true), featureFlags.getFeatureFlags()); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java index 1a3bcc6fd..11a229b0f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java @@ -33,6 +33,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.metrics.PushLatencyManager; import org.whispersystems.textsecuregcm.redis.AbstractRedisClusterTest; @@ -63,11 +64,12 @@ public class MessagePersisterIntegrationTest extends AbstractRedisClusterTest { final MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messagesDynamoDbRule.getDynamoDB(), MessagesDynamoDbRule.TABLE_NAME, Duration.ofDays(7)); final AccountsManager accountsManager = mock(AccountsManager.class); + final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); notificationExecutorService = Executors.newSingleThreadExecutor(); messagesCache = new MessagesCache(getRedisCluster(), getRedisCluster(), notificationExecutorService); messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, mock(PushLatencyManager.class)); - messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, mock(FeatureFlagsManager.class), PERSIST_DELAY); + messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, PERSIST_DELAY); account = mock(Account.class); @@ -76,6 +78,7 @@ public class MessagePersisterIntegrationTest extends AbstractRedisClusterTest { when(account.getNumber()).thenReturn("+18005551234"); when(account.getUuid()).thenReturn(accountUuid); when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account)); + when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); messagesCache.start(); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java index 4ce3a270d..2884a2072 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java @@ -32,6 +32,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.redis.AbstractRedisClusterTest; @@ -54,7 +55,8 @@ public class MessagePersisterTest extends AbstractRedisClusterTest { public void setUp() throws Exception { super.setUp(); - final MessagesManager messagesManager = mock(MessagesManager.class); + final MessagesManager messagesManager = mock(MessagesManager.class); + final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); messagesDynamoDb = mock(MessagesDynamoDb.class); accountsManager = mock(AccountsManager.class); @@ -63,10 +65,11 @@ public class MessagePersisterTest extends AbstractRedisClusterTest { when(accountsManager.get(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account)); when(account.getNumber()).thenReturn(DESTINATION_ACCOUNT_NUMBER); + when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); notificationExecutorService = Executors.newSingleThreadExecutor(); messagesCache = new MessagesCache(getRedisCluster(), getRedisCluster(), notificationExecutorService); - messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, mock(FeatureFlagsManager.class), PERSIST_DELAY); + messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, PERSIST_DELAY); doAnswer(invocation -> { final UUID destinationUuid = invocation.getArgument(0); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTaskTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTaskTest.java deleted file mode 100644 index 208f103f1..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/workers/DeleteFeatureFlagTaskTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import org.junit.Before; -import org.junit.Test; -import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager; - -import java.io.PrintWriter; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class DeleteFeatureFlagTaskTest { - - private FeatureFlagsManager featureFlagsManager; - - @Before - public void setUp() { - featureFlagsManager = mock(FeatureFlagsManager.class); - - when(featureFlagsManager.getAllFlags()).thenReturn(Collections.emptyMap()); - } - - @Test - public void testExecute() { - final DeleteFeatureFlagTask task = new DeleteFeatureFlagTask(featureFlagsManager); - - task.execute(Map.of("flag", List.of("test-flag-1", "test-flag-2")), mock(PrintWriter.class)); - verify(featureFlagsManager).deleteFeatureFlag("test-flag-1"); - verify(featureFlagsManager).deleteFeatureFlag("test-flag-2"); - } -} \ No newline at end of file diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTaskTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTaskTest.java deleted file mode 100644 index 33fec542d..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/workers/SetFeatureFlagTaskTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import org.junit.Before; -import org.junit.Test; -import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager; - -import java.io.PrintWriter; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class SetFeatureFlagTaskTest { - - private FeatureFlagsManager featureFlagsManager; - - @Before - public void setUp() { - featureFlagsManager = mock(FeatureFlagsManager.class); - - when(featureFlagsManager.getAllFlags()).thenReturn(Collections.emptyMap()); - } - - @Test - public void testExecute() { - final SetFeatureFlagTask task = new SetFeatureFlagTask(featureFlagsManager); - - task.execute(Map.of("flag", List.of("test-flag"), "active", List.of("true")), mock(PrintWriter.class)); - - verify(featureFlagsManager).setFeatureFlag("test-flag", true); - } -} \ No newline at end of file