Add a single-shot command for removing expired accounts

This commit is contained in:
Jon Chambers
2023-08-30 13:03:19 -04:00
committed by Jon Chambers
parent 6fd1c84126
commit c3c7329ebb
3 changed files with 196 additions and 0 deletions

View File

@@ -215,6 +215,7 @@ import org.whispersystems.textsecuregcm.workers.CrawlAccountsCommand;
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
import org.whispersystems.textsecuregcm.workers.MigrateSignedECPreKeysCommand;
import org.whispersystems.textsecuregcm.workers.RemoveExpiredAccountsCommand;
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
@@ -271,6 +272,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand());
bootstrap.addCommand(new MessagePersisterServiceCommand());
bootstrap.addCommand(new MigrateSignedECPreKeysCommand());
bootstrap.addCommand(new RemoveExpiredAccountsCommand(Clock.systemUTC()));
}
@Override

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.workers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import net.sourceforge.argparse4j.inf.Subparser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ParallelFlux;
public class RemoveExpiredAccountsCommand extends AbstractSinglePassCrawlAccountsCommand {
private final Clock clock;
@VisibleForTesting
static final Duration MAX_IDLE_DURATION = Duration.ofDays(180);
@VisibleForTesting
static final String DRY_RUN_ARGUMENT = "dry-run";
private static final int MAX_CONCURRENCY = 16;
private static final String DELETED_ACCOUNT_COUNTER_NAME =
name(RemoveExpiredAccountsCommand.class, "deletedAccounts");
private static final Logger log = LoggerFactory.getLogger(RemoveExpiredAccountsCommand.class);
public RemoveExpiredAccountsCommand(final Clock clock) {
super("remove-expired-accounts", "Removes all accounts that have been idle for more than a set period of time");
this.clock = clock;
}
@Override
public void configure(final Subparser subparser) {
super.configure(subparser);
subparser.addArgument("--dry-run")
.type(Boolean.class)
.dest(DRY_RUN_ARGUMENT)
.required(false)
.setDefault(true)
.help("If true, don't actually delete accounts");
}
@Override
protected void crawlAccounts(final ParallelFlux<Account> accounts) {
final boolean isDryRun = getNamespace().getBoolean(DRY_RUN_ARGUMENT);
final Counter deletedAccountCounter =
Metrics.counter(DELETED_ACCOUNT_COUNTER_NAME, "dryRun", String.valueOf(isDryRun));
accounts.filter(this::isExpired)
.sequential()
.flatMap(expiredAccount -> {
final Mono<Void> deleteAccountMono = isDryRun
? Mono.empty()
: Mono.fromFuture(() -> getCommandDependencies().accountsManager().delete(expiredAccount, AccountsManager.DeletionReason.EXPIRED));
return deleteAccountMono
.doOnSuccess(ignored -> deletedAccountCounter.increment())
.onErrorResume(throwable -> {
log.warn("Failed to delete account {}", expiredAccount.getUuid(), throwable);
return Mono.empty();
});
}, MAX_CONCURRENCY)
.then()
.block();
}
@VisibleForTesting
boolean isExpired(final Account account) {
return Instant.ofEpochMilli(account.getLastSeen()).plus(MAX_IDLE_DURATION).isBefore(clock.instant());
}
}