diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index d49ad1391..32609307c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -261,11 +261,8 @@ import org.whispersystems.textsecuregcm.workers.BackupMetricsCommand; import org.whispersystems.textsecuregcm.workers.CertificateCommand; import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand; import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; -import org.whispersystems.textsecuregcm.workers.DiscardPushNotificationExperimentSamplesCommand; -import org.whispersystems.textsecuregcm.workers.FinishPushNotificationExperimentCommand; import org.whispersystems.textsecuregcm.workers.IdleDeviceNotificationSchedulerFactory; import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand; -import org.whispersystems.textsecuregcm.workers.NotifyIdleDevicesWithMessagesExperimentFactory; import org.whispersystems.textsecuregcm.workers.NotifyIdleDevicesWithoutMessagesCommand; import org.whispersystems.textsecuregcm.workers.ProcessScheduledJobsServiceCommand; import org.whispersystems.textsecuregcm.workers.RemoveExpiredAccountsCommand; @@ -276,7 +273,6 @@ import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSend import org.whispersystems.textsecuregcm.workers.ServerVersionCommand; import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask; import org.whispersystems.textsecuregcm.workers.SetUserDiscoverabilityCommand; -import org.whispersystems.textsecuregcm.workers.StartPushNotificationExperimentCommand; import org.whispersystems.textsecuregcm.workers.UnlinkDeviceCommand; import org.whispersystems.textsecuregcm.workers.ZkParamsCommand; import org.whispersystems.websocket.WebSocketResourceProviderFactory; @@ -334,21 +330,6 @@ public class WhisperServerService extends Application("start-notify-idle-devices-with-messages-experiment", - "Start an experiment to send push notifications to idle devices with pending messages", - new NotifyIdleDevicesWithMessagesExperimentFactory())); - - bootstrap.addCommand( - new FinishPushNotificationExperimentCommand<>("finish-notify-idle-devices-with-messages-experiment", - "Finish an experiment to send push notifications to idle devices with pending messages", - new NotifyIdleDevicesWithMessagesExperimentFactory())); - - bootstrap.addCommand( - new DiscardPushNotificationExperimentSamplesCommand("discard-notify-idle-devices-with-messages-samples", - "Discard samples from the \"notify idle devices with messages\" experiment", - new NotifyIdleDevicesWithMessagesExperimentFactory())); } @Override diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/NotifyIdleDevicesWithMessagesExperiment.java b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/NotifyIdleDevicesWithMessagesExperiment.java deleted file mode 100644 index 0430bd18d..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/NotifyIdleDevicesWithMessagesExperiment.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.whispersystems.textsecuregcm.experiment; - -import com.google.common.annotations.VisibleForTesting; -import org.whispersystems.textsecuregcm.entities.MessageProtos; -import org.whispersystems.textsecuregcm.identity.IdentityType; -import org.whispersystems.textsecuregcm.push.IdleDeviceNotificationScheduler; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.Device; -import org.whispersystems.textsecuregcm.storage.MessagesManager; -import reactor.core.publisher.Flux; -import java.time.Clock; -import java.time.Duration; -import java.time.LocalTime; -import java.util.concurrent.CompletableFuture; - -public class NotifyIdleDevicesWithMessagesExperiment extends IdleDevicePushNotificationExperiment { - - private final IdleDeviceNotificationScheduler idleDeviceNotificationScheduler; - private final MessagesManager messagesManager; - - @VisibleForTesting - static final Duration MIN_IDLE_DURATION = Duration.ofDays(3); - - @VisibleForTesting - static final Duration MAX_IDLE_DURATION = Duration.ofDays(14); - - @VisibleForTesting - static final LocalTime PREFERRED_NOTIFICATION_TIME = LocalTime.of(14, 0); - - public NotifyIdleDevicesWithMessagesExperiment(final IdleDeviceNotificationScheduler idleDeviceNotificationScheduler, - final MessagesManager messagesManager, - final Clock clock) { - - super(clock); - - this.idleDeviceNotificationScheduler = idleDeviceNotificationScheduler; - this.messagesManager = messagesManager; - } - - @Override - protected Duration getMinIdleDuration() { - return MIN_IDLE_DURATION; - } - - @Override - protected Duration getMaxIdleDuration() { - return MAX_IDLE_DURATION; - } - - @Override - public String getExperimentName() { - return "notify-idle-devices-with-messages"; - } - - @Override - public CompletableFuture isDeviceEligible(final Account account, final Device device) { - - if (!device.isPrimary()) { - return CompletableFuture.completedFuture(false); - } - - if (!hasPushToken(device)) { - return CompletableFuture.completedFuture(false); - } - - if (!isIdle(device)) { - return CompletableFuture.completedFuture(false); - } - - return Flux.from(messagesManager.getMessagesForDeviceReactive(account.getIdentifier(IdentityType.ACI), device, false)) - .any(MessageProtos.Envelope::getUrgent) - .toFuture(); - } - - @Override - public Class getStateClass() { - return DeviceLastSeenState.class; - } - - @Override - public CompletableFuture applyExperimentTreatment(final Account account, final Device device) { - return idleDeviceNotificationScheduler.scheduleNotification(account, device, PREFERRED_NOTIFICATION_TIME); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/NotifyIdleDevicesWithMessagesExperimentFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/NotifyIdleDevicesWithMessagesExperimentFactory.java deleted file mode 100644 index 9aab3afd5..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/NotifyIdleDevicesWithMessagesExperimentFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.whispersystems.textsecuregcm.workers; - -import org.whispersystems.textsecuregcm.WhisperServerConfiguration; -import org.whispersystems.textsecuregcm.configuration.DynamoDbTables; -import org.whispersystems.textsecuregcm.experiment.DeviceLastSeenState; -import org.whispersystems.textsecuregcm.experiment.NotifyIdleDevicesWithMessagesExperiment; -import org.whispersystems.textsecuregcm.experiment.PushNotificationExperiment; -import org.whispersystems.textsecuregcm.push.IdleDeviceNotificationScheduler; -import java.time.Clock; - -public class NotifyIdleDevicesWithMessagesExperimentFactory implements PushNotificationExperimentFactory { - - @Override - public PushNotificationExperiment buildExperiment(final CommandDependencies commandDependencies, - final WhisperServerConfiguration configuration) { - - final DynamoDbTables.TableWithExpiration tableConfiguration = configuration.getDynamoDbTables().getScheduledJobs(); - - final Clock clock = Clock.systemUTC(); - - return new NotifyIdleDevicesWithMessagesExperiment(new IdleDeviceNotificationScheduler( - commandDependencies.accountsManager(), - commandDependencies.pushNotificationManager(), - commandDependencies.dynamoDbAsyncClient(), - tableConfiguration.getTableName(), - tableConfiguration.getExpiration(), - clock), - commandDependencies.messagesManager(), - clock); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/experiment/NotifyIdleDevicesWithMessagesExperimentTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/experiment/NotifyIdleDevicesWithMessagesExperimentTest.java deleted file mode 100644 index 6f217cdce..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/experiment/NotifyIdleDevicesWithMessagesExperimentTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package org.whispersystems.textsecuregcm.experiment; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import java.time.Clock; -import java.time.Duration; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.whispersystems.textsecuregcm.entities.MessageProtos; -import org.whispersystems.textsecuregcm.identity.IdentityType; -import org.whispersystems.textsecuregcm.push.IdleDeviceNotificationScheduler; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.Device; -import org.whispersystems.textsecuregcm.storage.MessagesManager; -import reactor.core.publisher.Flux; - -class NotifyIdleDevicesWithMessagesExperimentTest extends IdleDevicePushNotificationExperimentTest { - - private IdleDeviceNotificationScheduler idleDeviceNotificationScheduler; - private MessagesManager messagesManager; - - private NotifyIdleDevicesWithMessagesExperiment experiment; - - @BeforeEach - void setUp() { - idleDeviceNotificationScheduler = mock(IdleDeviceNotificationScheduler.class); - messagesManager = mock(MessagesManager.class); - - experiment = new NotifyIdleDevicesWithMessagesExperiment(idleDeviceNotificationScheduler, - messagesManager, - Clock.fixed(CURRENT_TIME, ZoneId.systemDefault())); - } - - @Override - protected IdleDevicePushNotificationExperiment getExperiment() { - return experiment; - } - - @ParameterizedTest - @MethodSource - void isDeviceEligible(final Account account, - final Device device, - final boolean hasUrgentMessage, - final boolean expectEligible) { - - when(messagesManager.getMessagesForDeviceReactive(account.getIdentifier(IdentityType.ACI), device, false)) - .thenReturn(Flux.just(MessageProtos.Envelope.newBuilder().setUrgent(hasUrgentMessage).build())); - - assertEquals(expectEligible, experiment.isDeviceEligible(account, device).join()); - } - - private static List isDeviceEligible() { - final List arguments = new ArrayList<>(); - - final Account account = mock(Account.class); - when(account.getIdentifier(IdentityType.ACI)).thenReturn(UUID.randomUUID()); - when(account.getNumber()).thenReturn(PhoneNumberUtil.getInstance().format( - PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164)); - - { - // Idle primary device with push token and urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getApnId()).thenReturn("apns-token"); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.minus(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION).toEpochMilli()); - - arguments.add(Arguments.of(account, device, true, true)); - } - - { - // Idle non-primary device with push token and urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(false); - when(device.getApnId()).thenReturn("apns-token"); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.minus(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION).toEpochMilli()); - - arguments.add(Arguments.of(account, device, true, false)); - } - - { - // Idle primary device missing push token, but with messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.minus(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION).toEpochMilli()); - - arguments.add(Arguments.of(account, device, true, false)); - } - - { - // Idle primary device missing push token and with no urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.minus(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION).toEpochMilli()); - - arguments.add(Arguments.of(account, device, false, false)); - } - - { - // Idle primary device with push token, but no urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.minus(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION).toEpochMilli()); - when(device.getApnId()).thenReturn("apns-token"); - - arguments.add(Arguments.of(account, device, false, false)); - } - - { - // Active primary device with push token and urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); - when(device.getApnId()).thenReturn("apns-token"); - - arguments.add(Arguments.of(account, device, true, false)); - } - - { - // Active primary device missing push token, but with urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); - - arguments.add(Arguments.of(account, device, true, false)); - } - - { - // Active primary device missing push token and with no urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); - - arguments.add(Arguments.of(account, device, false, false)); - } - - { - // Active primary device with push token, but no urgent messages - final Device device = mock(Device.class); - when(device.isPrimary()).thenReturn(true); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); - when(device.getApnId()).thenReturn("apns-token"); - - arguments.add(Arguments.of(account, device, false, false)); - } - - return arguments; - } - - @ParameterizedTest - @MethodSource - void isIdle(final Duration idleDuration, final boolean expectIdle) { - final Device device = mock(Device.class); - when(device.getLastSeen()).thenReturn(CURRENT_TIME.minus(idleDuration).toEpochMilli()); - - assertEquals(expectIdle, experiment.isIdle(device)); - } - - private static List isIdle() { - return List.of( - Arguments.of(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION, true), - Arguments.of(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION.plusMillis(1), true), - Arguments.of(NotifyIdleDevicesWithMessagesExperiment.MIN_IDLE_DURATION.minusMillis(1), false), - Arguments.of(NotifyIdleDevicesWithMessagesExperiment.MAX_IDLE_DURATION, false), - Arguments.of(NotifyIdleDevicesWithMessagesExperiment.MAX_IDLE_DURATION.plusMillis(1), false), - Arguments.of(NotifyIdleDevicesWithMessagesExperiment.MAX_IDLE_DURATION.minusMillis(1), true) - ); - } - - @Test - void applyExperimentTreatment() { - final Account account = mock(Account.class); - final Device device = mock(Device.class); - - experiment.applyExperimentTreatment(account, device); - - verify(idleDeviceNotificationScheduler) - .scheduleNotification(account, device, NotifyIdleDevicesWithMessagesExperiment.PREFERRED_NOTIFICATION_TIME); - } -}