From 1182d159aa0eae89f9839728a05ae2e52882500f Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Tue, 14 May 2024 12:39:44 -0500 Subject: [PATCH] Move command boilerplate into a base class --- .../AbstractCommandWithDependencies.java | 75 +++++++++++++++ ...bstractSinglePassCrawlAccountsCommand.java | 35 +------ .../workers/BackupMetricsCommand.java | 96 +++++++------------ .../workers/DeleteUserCommand.java | 21 ++-- .../workers/RemoveExpiredBackupsCommand.java | 51 ++-------- .../SetUserDiscoverabilityCommand.java | 10 +- .../workers/UnlinkDeviceCommand.java | 33 +++---- 7 files changed, 148 insertions(+), 173 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractCommandWithDependencies.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractCommandWithDependencies.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractCommandWithDependencies.java new file mode 100644 index 000000000..d82cff3e6 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractCommandWithDependencies.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.core.Application; +import io.dropwizard.core.cli.Cli; +import io.dropwizard.core.cli.EnvironmentCommand; +import io.dropwizard.core.setup.Environment; +import net.sourceforge.argparse4j.inf.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; + +/** + * Base class for one-shot commands that use {@link CommandDependencies}. + *

+ * Override {@link #run(Environment, Namespace, WhisperServerConfiguration, CommandDependencies)} in a child class to + * let the parent class handle common initialization of dependencies, metrics, and logging. + */ +public abstract class AbstractCommandWithDependencies extends EnvironmentCommand { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + protected AbstractCommandWithDependencies(final Application application, + final String name, final String description) { + super(application, name, description); + } + + /** + * Run the command with the given initialized {@link CommandDependencies} + */ + protected abstract void run(final Environment environment, final Namespace namespace, + final WhisperServerConfiguration configuration, final CommandDependencies commandDependencies) throws Exception; + + @Override + protected void run(final Environment environment, final Namespace namespace, + final WhisperServerConfiguration configuration) throws Exception { + UncaughtExceptionHandler.register(); + final CommandDependencies commandDependencies = CommandDependencies.build(getName(), environment, configuration); + MetricsUtil.configureRegistries(configuration, environment, commandDependencies.dynamicConfigurationManager()); + + try { + logger.info("Starting command dependencies"); + environment.lifecycle().getManagedObjects().forEach(managedObject -> { + try { + managedObject.start(); + } catch (final Exception e) { + logger.error("Failed to start managed object", e); + throw new RuntimeException(e); + } + }); + + run(environment, namespace, configuration, commandDependencies); + + } finally { + logger.info("Stopping command dependencies"); + environment.lifecycle().getManagedObjects().forEach(managedObject -> { + try { + managedObject.stop(); + } catch (final Exception e) { + logger.error("Failed to stop managed object", e); + } + }); + } + } + + @Override + public void onError(final Cli cli, final Namespace namespace, final Throwable throwable) { + logger.error("Unhandled error", throwable); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java index e7133ed54..30237ca8c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java @@ -20,7 +20,7 @@ import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; -public abstract class AbstractSinglePassCrawlAccountsCommand extends EnvironmentCommand { +public abstract class AbstractSinglePassCrawlAccountsCommand extends AbstractCommandWithDependencies { private CommandDependencies commandDependencies; private Namespace namespace; @@ -59,12 +59,9 @@ public abstract class AbstractSinglePassCrawlAccountsCommand extends Environment @Override protected void run(final Environment environment, final Namespace namespace, - final WhisperServerConfiguration configuration) throws Exception { - - UncaughtExceptionHandler.register(); - + final WhisperServerConfiguration configuration, final CommandDependencies commandDependencies) throws Exception { this.namespace = namespace; - this.commandDependencies = CommandDependencies.build(getName(), environment, configuration); + this.commandDependencies = commandDependencies; final int segments = Objects.requireNonNull(namespace.getInt(SEGMENT_COUNT)); @@ -72,31 +69,7 @@ public abstract class AbstractSinglePassCrawlAccountsCommand extends Environment segments, Runtime.getRuntime().availableProcessors()); - try { - environment.lifecycle().getManagedObjects().forEach(managedObject -> { - try { - managedObject.start(); - } catch (final Exception e) { - logger.error("Failed to start managed object", e); - throw new RuntimeException(e); - } - }); - - crawlAccounts(commandDependencies.accountsManager().streamAllFromDynamo(segments, Schedulers.parallel())); - } finally { - environment.lifecycle().getManagedObjects().forEach(managedObject -> { - try { - managedObject.stop(); - } catch (final Exception e) { - logger.error("Failed to stop managed object", e); - } - }); - } - } - - @Override - public void onError(final Cli cli, final Namespace namespace, final Throwable throwable) { - logger.error("Unhandled error", throwable); + crawlAccounts(commandDependencies.accountsManager().streamAllFromDynamo(segments, Schedulers.parallel())); } protected abstract void crawlAccounts(final Flux accounts); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java index 2e3d9d6c1..96430236f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java @@ -29,7 +29,7 @@ import java.util.Objects; import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; -public class BackupMetricsCommand extends EnvironmentCommand { +public class BackupMetricsCommand extends AbstractCommandWithDependencies { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -61,70 +61,47 @@ public class BackupMetricsCommand extends EnvironmentCommand { - try { - managedObject.start(); - } catch (final Exception e) { - logger.error("Failed to start managed object", e); - throw new RuntimeException(e); - } - }); + final DistributionSummary numObjectsMediaTier = Metrics.summary(name(getClass(), "numObjects"), + "tier", BackupLevel.MEDIA.name()); + final DistributionSummary bytesUsedMediaTier = Metrics.summary(name(getClass(), "bytesUsed"), + "tier", BackupLevel.MEDIA.name()); + final DistributionSummary numObjectsMessagesTier = Metrics.summary(name(getClass(), "numObjects"), + "tier", BackupLevel.MESSAGES.name()); + final DistributionSummary bytesUsedMessagesTier = Metrics.summary(name(getClass(), "bytesUsed"), + "tier", BackupLevel.MESSAGES.name()); - final DistributionSummary numObjectsMediaTier = Metrics.summary(name(getClass(), "numObjects"), - "tier", BackupLevel.MEDIA.name()); - final DistributionSummary bytesUsedMediaTier = Metrics.summary(name(getClass(), "bytesUsed"), - "tier", BackupLevel.MEDIA.name()); - final DistributionSummary numObjectsMessagesTier = Metrics.summary(name(getClass(), "numObjects"), - "tier", BackupLevel.MESSAGES.name()); - final DistributionSummary bytesUsedMessagesTier = Metrics.summary(name(getClass(), "bytesUsed"), - "tier", BackupLevel.MESSAGES.name()); + final DistributionSummary timeSinceLastRefresh = Metrics.summary(name(getClass(), + "timeSinceLastRefresh")); + final DistributionSummary timeSinceLastMediaRefresh = Metrics.summary(name(getClass(), + "timeSinceLastMediaRefresh")); + final String backupsCounterName = name(getClass(), "backups"); - final DistributionSummary timeSinceLastRefresh = Metrics.summary(name(getClass(), - "timeSinceLastRefresh")); - final DistributionSummary timeSinceLastMediaRefresh = Metrics.summary(name(getClass(), - "timeSinceLastMediaRefresh")); - final String backupsCounterName = name(getClass(), "backups"); - - final BackupManager backupManager = commandDependencies.backupManager(); - final Long backupsExpired = backupManager - .listBackupAttributes(segments, Schedulers.parallel()) - .doOnNext(backupMetadata -> { - final boolean subscribed = backupMetadata.lastMediaRefresh().equals(backupMetadata.lastRefresh()); - if (subscribed) { - numObjectsMediaTier.record(backupMetadata.numObjects()); - bytesUsedMediaTier.record(backupMetadata.bytesUsed()); - } else { - numObjectsMessagesTier.record(backupMetadata.numObjects()); - bytesUsedMessagesTier.record(backupMetadata.bytesUsed()); - } - timeSinceLastRefresh.record(timeSince(backupMetadata.lastRefresh()).getSeconds()); - timeSinceLastMediaRefresh.record(timeSince(backupMetadata.lastMediaRefresh()).getSeconds()); - Metrics.counter(backupsCounterName, "subscribed", String.valueOf(subscribed)).increment(); - }) - .count() - .block(); - logger.info("Crawled {} backups", backupsExpired); - } finally { - environment.lifecycle().getManagedObjects().forEach(managedObject -> { - try { - managedObject.stop(); - } catch (final Exception e) { - logger.error("Failed to stop managed object", e); - } - }); - } + final BackupManager backupManager = commandDependencies.backupManager(); + final Long backupsExpired = backupManager + .listBackupAttributes(segments, Schedulers.parallel()) + .doOnNext(backupMetadata -> { + final boolean subscribed = backupMetadata.lastMediaRefresh().equals(backupMetadata.lastRefresh()); + if (subscribed) { + numObjectsMediaTier.record(backupMetadata.numObjects()); + bytesUsedMediaTier.record(backupMetadata.bytesUsed()); + } else { + numObjectsMessagesTier.record(backupMetadata.numObjects()); + bytesUsedMessagesTier.record(backupMetadata.bytesUsed()); + } + timeSinceLastRefresh.record(timeSince(backupMetadata.lastRefresh()).getSeconds()); + timeSinceLastMediaRefresh.record(timeSince(backupMetadata.lastMediaRefresh()).getSeconds()); + Metrics.counter(backupsCounterName, "subscribed", String.valueOf(subscribed)).increment(); + }) + .count() + .block(); + logger.info("Crawled {} backups", backupsExpired); } private Duration timeSince(Instant t) { @@ -134,9 +111,4 @@ public class BackupMetricsCommand extends EnvironmentCommand { +public class DeleteUserCommand extends AbstractCommandWithDependencies { private final Logger logger = LoggerFactory.getLogger(DeleteUserCommand.class); @@ -36,22 +34,17 @@ public class DeleteUserCommand extends EnvironmentCommand { +public class RemoveExpiredBackupsCommand extends AbstractCommandWithDependencies { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -89,12 +86,7 @@ public class RemoveExpiredBackupsCommand extends EnvironmentCommand { - try { - managedObject.start(); - } catch (final Exception e) { - logger.error("Failed to start managed object", e); - throw new RuntimeException(e); - } - }); - final BackupManager backupManager = commandDependencies.backupManager(); - final long backupsExpired = backupManager - .getExpiredBackups(segments, Schedulers.parallel(), clock.instant().minus(gracePeriod)) - .flatMap(expiredBackup -> removeExpiredBackup(backupManager, expiredBackup, dryRun), concurrency) - .filter(Boolean.TRUE::equals) - .count() - .block(); - logger.info("Expired {} backups", backupsExpired); - } finally { - environment.lifecycle().getManagedObjects().forEach(managedObject -> { - try { - managedObject.stop(); - } catch (final Exception e) { - logger.error("Failed to stop managed object", e); - } - }); - } + final BackupManager backupManager = commandDependencies.backupManager(); + final long backupsExpired = backupManager + .getExpiredBackups(segments, Schedulers.parallel(), clock.instant().minus(gracePeriod)) + .flatMap(expiredBackup -> removeExpiredBackup(backupManager, expiredBackup, dryRun), concurrency) + .filter(Boolean.TRUE::equals) + .count() + .block(); + logger.info("Expired {} backups", backupsExpired); } private Mono removeExpiredBackup( @@ -162,9 +136,4 @@ public class RemoveExpiredBackupsCommand extends EnvironmentCommand { +public class SetUserDiscoverabilityCommand extends AbstractCommandWithDependencies { public SetUserDiscoverabilityCommand() { @@ -48,12 +46,10 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand maybeAccount; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java index c6b9b5669..b73d6cde5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java @@ -19,7 +19,7 @@ import org.whispersystems.textsecuregcm.WhisperServerConfiguration; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; -public class UnlinkDeviceCommand extends EnvironmentCommand { +public class UnlinkDeviceCommand extends AbstractCommandWithDependencies { public UnlinkDeviceCommand() { super(new Application<>() { @@ -49,25 +49,22 @@ public class UnlinkDeviceCommand extends EnvironmentCommand deviceIds = namespace.getList("deviceIds"); - final UUID aci = UUID.fromString(namespace.getString("uuid").trim()); - final List deviceIds = namespace.getList("deviceIds"); + Account account = deps.accountsManager().getByAccountIdentifier(aci) + .orElseThrow(() -> new IllegalArgumentException("account id " + aci + " does not exist")); - final CommandDependencies deps = CommandDependencies.build("unlink-device", environment, configuration); + if (deviceIds.contains(Device.PRIMARY_ID)) { + throw new IllegalArgumentException("cannot delete primary device"); + } - Account account = deps.accountsManager().getByAccountIdentifier(aci) - .orElseThrow(() -> new IllegalArgumentException("account id " + aci + " does not exist")); - - if (deviceIds.contains(Device.PRIMARY_ID)) { - throw new IllegalArgumentException("cannot delete primary device"); - } - - for (byte deviceId : deviceIds) { - /** see {@link org.whispersystems.textsecuregcm.controllers.DeviceController#removeDevice} */ - System.out.format("Removing device %s::%d\n", aci, deviceId); - deps.accountsManager().removeDevice(account, deviceId).join(); - } + for (byte deviceId : deviceIds) { + /** see {@link org.whispersystems.textsecuregcm.controllers.DeviceController#removeDevice} */ + System.out.format("Removing device %s::%d\n", aci, deviceId); + deps.accountsManager().removeDevice(account, deviceId).join(); + } } }