Remove recurring background VOIP notification machinery
This commit is contained in:
		
							parent
							
								
									3ed142d0a9
								
							
						
					
					
						commit
						b693cb98d0
					
				| 
						 | 
					@ -141,18 +141,6 @@ public class PushNotificationManager {
 | 
				
			||||||
              result.errorCode(),
 | 
					              result.errorCode(),
 | 
				
			||||||
              result.unregisteredTimestamp());
 | 
					              result.unregisteredTimestamp());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (result.accepted() &&
 | 
					 | 
				
			||||||
            pushNotification.tokenType() == PushNotification.TokenType.APN_VOIP &&
 | 
					 | 
				
			||||||
            pushNotification.notificationType() == PushNotification.NotificationType.NOTIFICATION &&
 | 
					 | 
				
			||||||
            pushNotification.destination() != null &&
 | 
					 | 
				
			||||||
            pushNotification.destinationDevice() != null) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          pushNotificationScheduler.scheduleRecurringApnsVoipNotification(
 | 
					 | 
				
			||||||
                  pushNotification.destination(),
 | 
					 | 
				
			||||||
                  pushNotification.destinationDevice())
 | 
					 | 
				
			||||||
              .whenComplete(logErrors());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        logger.debug("Failed to deliver {} push notification to {} ({})",
 | 
					        logger.debug("Failed to deliver {} push notification to {} ({})",
 | 
				
			||||||
            pushNotification.notificationType(), pushNotification.deviceToken(), pushNotification.tokenType(),
 | 
					            pushNotification.notificationType(), pushNotification.deviceToken(), pushNotification.tokenType(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,20 +19,17 @@ import java.io.IOException;
 | 
				
			||||||
import java.time.Clock;
 | 
					import java.time.Clock;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.util.Collections;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
import java.util.concurrent.CompletionStage;
 | 
					import java.util.concurrent.CompletionStage;
 | 
				
			||||||
import java.util.concurrent.TimeUnit;
 | 
					 | 
				
			||||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
					import java.util.concurrent.atomic.AtomicBoolean;
 | 
				
			||||||
import java.util.concurrent.atomic.AtomicLong;
 | 
					import java.util.concurrent.atomic.AtomicLong;
 | 
				
			||||||
import java.util.function.BiFunction;
 | 
					import java.util.function.BiFunction;
 | 
				
			||||||
import org.apache.commons.lang3.StringUtils;
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
import org.slf4j.Logger;
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
import org.slf4j.LoggerFactory;
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
 | 
					import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
 | 
					import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Account;
 | 
					import org.whispersystems.textsecuregcm.storage.Account;
 | 
				
			||||||
| 
						 | 
					@ -41,14 +38,12 @@ import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Pair;
 | 
					import org.whispersystems.textsecuregcm.util.Pair;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.RedisClusterUtil;
 | 
					import org.whispersystems.textsecuregcm.util.RedisClusterUtil;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Util;
 | 
					import org.whispersystems.textsecuregcm.util.Util;
 | 
				
			||||||
import reactor.core.publisher.Flux;
 | 
					 | 
				
			||||||
import reactor.core.publisher.Mono;
 | 
					import reactor.core.publisher.Mono;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class PushNotificationScheduler implements Managed {
 | 
					public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final Logger logger = LoggerFactory.getLogger(PushNotificationScheduler.class);
 | 
					  private static final Logger logger = LoggerFactory.getLogger(PushNotificationScheduler.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final String PENDING_RECURRING_VOIP_NOTIFICATIONS_KEY_PREFIX = "PENDING_APN";
 | 
					 | 
				
			||||||
  private static final String PENDING_BACKGROUND_NOTIFICATIONS_KEY_PREFIX = "PENDING_BACKGROUND_APN";
 | 
					  private static final String PENDING_BACKGROUND_NOTIFICATIONS_KEY_PREFIX = "PENDING_BACKGROUND_APN";
 | 
				
			||||||
  private static final String LAST_BACKGROUND_NOTIFICATION_TIMESTAMP_KEY_PREFIX = "LAST_BACKGROUND_NOTIFICATION";
 | 
					  private static final String LAST_BACKGROUND_NOTIFICATION_TIMESTAMP_KEY_PREFIX = "LAST_BACKGROUND_NOTIFICATION";
 | 
				
			||||||
  private static final String PENDING_DELAYED_NOTIFICATIONS_KEY_PREFIX = "DELAYED";
 | 
					  private static final String PENDING_DELAYED_NOTIFICATIONS_KEY_PREFIX = "DELAYED";
 | 
				
			||||||
| 
						 | 
					@ -56,11 +51,6 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  static final String NEXT_SLOT_TO_PROCESS_KEY = "pending_notification_next_slot";
 | 
					  static final String NEXT_SLOT_TO_PROCESS_KEY = "pending_notification_next_slot";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final Counter delivered = Metrics.counter("chat.ApnPushNotificationScheduler.voip_delivered");
 | 
					 | 
				
			||||||
  private static final Counter sent = Metrics.counter("chat.ApnPushNotificationScheduler.voip_sent");
 | 
					 | 
				
			||||||
  private static final Counter retry = Metrics.counter("chat.ApnPushNotificationScheduler.voip_retry");
 | 
					 | 
				
			||||||
  private static final Counter evicted = Metrics.counter("chat.ApnPushNotificationScheduler.voip_evicted");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private static final Counter BACKGROUND_NOTIFICATION_SCHEDULED_COUNTER = Metrics.counter(name(PushNotificationScheduler.class, "backgroundNotification", "scheduled"));
 | 
					  private static final Counter BACKGROUND_NOTIFICATION_SCHEDULED_COUNTER = Metrics.counter(name(PushNotificationScheduler.class, "backgroundNotification", "scheduled"));
 | 
				
			||||||
  private static final String BACKGROUND_NOTIFICATION_SENT_COUNTER_NAME = name(PushNotificationScheduler.class, "backgroundNotification", "sent");
 | 
					  private static final String BACKGROUND_NOTIFICATION_SENT_COUNTER_NAME = name(PushNotificationScheduler.class, "backgroundNotification", "sent");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,10 +65,6 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
  private final FaultTolerantRedisCluster pushSchedulingCluster;
 | 
					  private final FaultTolerantRedisCluster pushSchedulingCluster;
 | 
				
			||||||
  private final Clock clock;
 | 
					  private final Clock clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final ClusterLuaScript getPendingVoipDestinationsScript;
 | 
					 | 
				
			||||||
  private final ClusterLuaScript insertPendingVoipDestinationScript;
 | 
					 | 
				
			||||||
  private final ClusterLuaScript removePendingVoipDestinationScript;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private final ClusterLuaScript scheduleBackgroundApnsNotificationScript;
 | 
					  private final ClusterLuaScript scheduleBackgroundApnsNotificationScript;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final Thread[] workerThreads;
 | 
					  private final Thread[] workerThreads;
 | 
				
			||||||
| 
						 | 
					@ -117,37 +103,7 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
      final int slot = (int) (pushSchedulingCluster.withCluster(connection ->
 | 
					      final int slot = (int) (pushSchedulingCluster.withCluster(connection ->
 | 
				
			||||||
          connection.sync().incr(NEXT_SLOT_TO_PROCESS_KEY)) % SlotHash.SLOT_COUNT);
 | 
					          connection.sync().incr(NEXT_SLOT_TO_PROCESS_KEY)) % SlotHash.SLOT_COUNT);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return processRecurringApnsVoipNotifications(slot) +
 | 
					      return processScheduledBackgroundApnsNotifications(slot) + processScheduledDelayedNotifications(slot);
 | 
				
			||||||
          processScheduledBackgroundApnsNotifications(slot) +
 | 
					 | 
				
			||||||
          processScheduledDelayedNotifications(slot);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @VisibleForTesting
 | 
					 | 
				
			||||||
    long processRecurringApnsVoipNotifications(final int slot) {
 | 
					 | 
				
			||||||
      List<String> pendingDestinations;
 | 
					 | 
				
			||||||
      long entriesProcessed = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      do {
 | 
					 | 
				
			||||||
        pendingDestinations = getPendingDestinationsForRecurringApnsVoipNotifications(slot, PAGE_SIZE);
 | 
					 | 
				
			||||||
        entriesProcessed += pendingDestinations.size();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Flux.fromIterable(pendingDestinations)
 | 
					 | 
				
			||||||
            .flatMap(destination -> Mono.fromFuture(() -> getAccountAndDeviceFromPairString(destination))
 | 
					 | 
				
			||||||
                .flatMap(maybeAccountAndDevice -> {
 | 
					 | 
				
			||||||
                  if (maybeAccountAndDevice.isPresent()) {
 | 
					 | 
				
			||||||
                    final Pair<Account, Device> accountAndDevice = maybeAccountAndDevice.get();
 | 
					 | 
				
			||||||
                    return Mono.fromFuture(() -> sendRecurringApnsVoipNotification(accountAndDevice.first(), accountAndDevice.second()));
 | 
					 | 
				
			||||||
                  } else {
 | 
					 | 
				
			||||||
                    final Pair<UUID, Byte> aciAndDeviceId = decodeAciAndDeviceId(destination);
 | 
					 | 
				
			||||||
                    return Mono.fromFuture(() -> removeRecurringApnsVoipNotificationEntry(aciAndDeviceId.first(), aciAndDeviceId.second()))
 | 
					 | 
				
			||||||
                        .then();
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                }), maxConcurrency)
 | 
					 | 
				
			||||||
            .then()
 | 
					 | 
				
			||||||
            .block();
 | 
					 | 
				
			||||||
      } while (!pendingDestinations.isEmpty());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return entriesProcessed;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @VisibleForTesting
 | 
					    @VisibleForTesting
 | 
				
			||||||
| 
						 | 
					@ -216,13 +172,6 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
    this.pushSchedulingCluster = pushSchedulingCluster;
 | 
					    this.pushSchedulingCluster = pushSchedulingCluster;
 | 
				
			||||||
    this.clock = clock;
 | 
					    this.clock = clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.getPendingVoipDestinationsScript = ClusterLuaScript.fromResource(pushSchedulingCluster, "lua/apn/get.lua",
 | 
					 | 
				
			||||||
        ScriptOutputType.MULTI);
 | 
					 | 
				
			||||||
    this.insertPendingVoipDestinationScript = ClusterLuaScript.fromResource(pushSchedulingCluster, "lua/apn/insert.lua",
 | 
					 | 
				
			||||||
        ScriptOutputType.VALUE);
 | 
					 | 
				
			||||||
    this.removePendingVoipDestinationScript = ClusterLuaScript.fromResource(pushSchedulingCluster, "lua/apn/remove.lua",
 | 
					 | 
				
			||||||
        ScriptOutputType.INTEGER);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.scheduleBackgroundApnsNotificationScript = ClusterLuaScript.fromResource(pushSchedulingCluster,
 | 
					    this.scheduleBackgroundApnsNotificationScript = ClusterLuaScript.fromResource(pushSchedulingCluster,
 | 
				
			||||||
        "lua/apn/schedule_background_notification.lua", ScriptOutputType.VALUE);
 | 
					        "lua/apn/schedule_background_notification.lua", ScriptOutputType.VALUE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -233,17 +182,6 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Schedule a recurring VOIP notification until {@link this#cancelScheduledNotifications} is called or the device is
 | 
					 | 
				
			||||||
   * removed
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * @return A CompletionStage that completes when the recurring notification has successfully been scheduled
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public CompletionStage<Void> scheduleRecurringApnsVoipNotification(Account account, Device device) {
 | 
					 | 
				
			||||||
    sent.increment();
 | 
					 | 
				
			||||||
    return insertRecurringApnsVoipNotificationEntry(account.getIdentifier(IdentityType.ACI), device.getId(), clock.millis() + (15 * 1000), (15 * 1000));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Schedule a background APNs notification to be sent some time in the future.
 | 
					   * Schedule a background APNs notification to be sent some time in the future.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
| 
						 | 
					@ -292,32 +230,16 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Cancel a scheduled recurring VOIP notification
 | 
					   * Cancel scheduled notifications for the given account and device.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @return A CompletionStage that completes when the scheduled task has been cancelled.
 | 
					   * @return A CompletionStage that completes when the scheduled notification has been cancelled.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public CompletionStage<Void> cancelScheduledNotifications(Account account, Device device) {
 | 
					  public CompletionStage<Void> cancelScheduledNotifications(Account account, Device device) {
 | 
				
			||||||
    return CompletableFuture.allOf(
 | 
					    return CompletableFuture.allOf(
 | 
				
			||||||
        cancelRecurringApnsVoipNotifications(account, device),
 | 
					 | 
				
			||||||
        cancelBackgroundApnsNotifications(account, device),
 | 
					        cancelBackgroundApnsNotifications(account, device),
 | 
				
			||||||
        cancelDelayedNotifications(account, device));
 | 
					        cancelDelayedNotifications(account, device));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private CompletableFuture<Void> cancelRecurringApnsVoipNotifications(final Account account, final Device device) {
 | 
					 | 
				
			||||||
    return removeRecurringApnsVoipNotificationEntry(account.getIdentifier(IdentityType.ACI), device.getId())
 | 
					 | 
				
			||||||
        .thenCompose(removed -> {
 | 
					 | 
				
			||||||
          if (removed) {
 | 
					 | 
				
			||||||
            delivered.increment();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return pushSchedulingCluster.withCluster(connection ->
 | 
					 | 
				
			||||||
              connection.async().zrem(
 | 
					 | 
				
			||||||
                  getPendingBackgroundApnsNotificationQueueKey(account, device),
 | 
					 | 
				
			||||||
                  encodeAciAndDeviceId(account, device)));
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .thenRun(Util.NOOP)
 | 
					 | 
				
			||||||
        .toCompletableFuture();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  CompletableFuture<Void> cancelBackgroundApnsNotifications(final Account account, final Device device) {
 | 
					  CompletableFuture<Void> cancelBackgroundApnsNotifications(final Account account, final Device device) {
 | 
				
			||||||
    return pushSchedulingCluster.withCluster(connection -> connection.async()
 | 
					    return pushSchedulingCluster.withCluster(connection -> connection.async()
 | 
				
			||||||
| 
						 | 
					@ -353,21 +275,6 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private CompletableFuture<Void> sendRecurringApnsVoipNotification(final Account account, final Device device) {
 | 
					 | 
				
			||||||
    if (StringUtils.isBlank(device.getVoipApnId())) {
 | 
					 | 
				
			||||||
      return removeRecurringApnsVoipNotificationEntry(account.getIdentifier(IdentityType.ACI), device.getId())
 | 
					 | 
				
			||||||
          .thenRun(Util.NOOP);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (device.getLastSeen() < clock.millis() - TimeUnit.DAYS.toMillis(7)) {
 | 
					 | 
				
			||||||
      return removeRecurringApnsVoipNotificationEntry(account.getIdentifier(IdentityType.ACI), device.getId())
 | 
					 | 
				
			||||||
          .thenRun(evicted::increment);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return apnSender.sendNotification(new PushNotification(device.getVoipApnId(), PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true))
 | 
					 | 
				
			||||||
        .thenRun(retry::increment);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  CompletableFuture<Void> sendBackgroundApnsNotification(final Account account, final Device device) {
 | 
					  CompletableFuture<Void> sendBackgroundApnsNotification(final Account account, final Device device) {
 | 
				
			||||||
    if (StringUtils.isBlank(device.getApnId())) {
 | 
					    if (StringUtils.isBlank(device.getApnId())) {
 | 
				
			||||||
| 
						 | 
					@ -443,48 +350,6 @@ public class PushNotificationScheduler implements Managed {
 | 
				
			||||||
            .flatMap(account -> account.getDevice(aciAndDeviceId.second()).map(device -> new Pair<>(account, device))));
 | 
					            .flatMap(account -> account.getDevice(aciAndDeviceId.second()).map(device -> new Pair<>(account, device))));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private CompletableFuture<Boolean> removeRecurringApnsVoipNotificationEntry(final UUID aci, final byte deviceId) {
 | 
					 | 
				
			||||||
    final String endpoint = getVoipEndpointKey(aci, deviceId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return removePendingVoipDestinationScript.executeAsync(
 | 
					 | 
				
			||||||
        List.of(getPendingRecurringApnsVoipNotificationQueueKey(endpoint), endpoint), Collections.emptyList())
 | 
					 | 
				
			||||||
        .thenApply(result -> ((long) result) > 0);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @SuppressWarnings("unchecked")
 | 
					 | 
				
			||||||
  @VisibleForTesting
 | 
					 | 
				
			||||||
  List<String> getPendingDestinationsForRecurringApnsVoipNotifications(final int slot, final int limit) {
 | 
					 | 
				
			||||||
    return (List<String>) getPendingVoipDestinationsScript.execute(
 | 
					 | 
				
			||||||
        List.of(getPendingRecurringApnsVoipNotificationQueueKey(slot)),
 | 
					 | 
				
			||||||
        List.of(String.valueOf(clock.millis()), String.valueOf(limit)));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @SuppressWarnings("SameParameterValue")
 | 
					 | 
				
			||||||
  private CompletionStage<Void> insertRecurringApnsVoipNotificationEntry(final UUID aci, final byte deviceId, final long timestamp, final long interval) {
 | 
					 | 
				
			||||||
    final String endpoint = getVoipEndpointKey(aci, deviceId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return insertPendingVoipDestinationScript.executeAsync(
 | 
					 | 
				
			||||||
        List.of(getPendingRecurringApnsVoipNotificationQueueKey(endpoint), endpoint),
 | 
					 | 
				
			||||||
        List.of(String.valueOf(timestamp),
 | 
					 | 
				
			||||||
            String.valueOf(interval),
 | 
					 | 
				
			||||||
            aci.toString(),
 | 
					 | 
				
			||||||
            String.valueOf(deviceId)))
 | 
					 | 
				
			||||||
        .thenRun(Util.NOOP);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VisibleForTesting
 | 
					 | 
				
			||||||
  static String getVoipEndpointKey(final UUID aci, final byte deviceId) {
 | 
					 | 
				
			||||||
    return "apn_device::{" + aci + "::" + deviceId + "}";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private static String getPendingRecurringApnsVoipNotificationQueueKey(final String endpoint) {
 | 
					 | 
				
			||||||
    return getPendingRecurringApnsVoipNotificationQueueKey(SlotHash.getSlot(endpoint));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private static String getPendingRecurringApnsVoipNotificationQueueKey(final int slot) {
 | 
					 | 
				
			||||||
    return PENDING_RECURRING_VOIP_NOTIFICATIONS_KEY_PREFIX + "::{" + RedisClusterUtil.getMinimalHashTag(slot) + "}";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  static String getPendingBackgroundApnsNotificationQueueKey(final Account account, final Device device) {
 | 
					  static String getPendingBackgroundApnsNotificationQueueKey(final Account account, final Device device) {
 | 
				
			||||||
    return getPendingBackgroundApnsNotificationQueueKey(SlotHash.getSlot(encodeAciAndDeviceId(account, device)));
 | 
					    return getPendingBackgroundApnsNotificationQueueKey(SlotHash.getSlot(encodeAciAndDeviceId(account, device)));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,70 +0,0 @@
 | 
				
			||||||
local pendingNotificationQueue = KEYS[1]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
local maxTime = ARGV[1]
 | 
					 | 
				
			||||||
local limit = ARGV[2]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
local hgetall = function (key)
 | 
					 | 
				
			||||||
    local bulk = redis.call('HGETALL', key)
 | 
					 | 
				
			||||||
    local result = {}
 | 
					 | 
				
			||||||
    local nextkey
 | 
					 | 
				
			||||||
    for i, v in ipairs(bulk) do
 | 
					 | 
				
			||||||
        if i % 2 == 1 then
 | 
					 | 
				
			||||||
            nextkey = v
 | 
					 | 
				
			||||||
        else
 | 
					 | 
				
			||||||
            result[nextkey] = v
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
    return result
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
local getNextInterval = function(interval)
 | 
					 | 
				
			||||||
    if interval < 20000 then
 | 
					 | 
				
			||||||
        return 20000
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if interval < 40000 then
 | 
					 | 
				
			||||||
        return 40000
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if interval < 80000 then
 | 
					 | 
				
			||||||
        return 80000
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if interval < 160000 then
 | 
					 | 
				
			||||||
        return 160000
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if interval < 600000 then
 | 
					 | 
				
			||||||
        return 600000
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if interval < 1800000 then
 | 
					 | 
				
			||||||
        return 1800000
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return 3600000
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
local results  = redis.call("ZRANGE", pendingNotificationQueue, 0, maxTime, "BYSCORE", "LIMIT", 0, limit)
 | 
					 | 
				
			||||||
local collated = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if results and next(results) then
 | 
					 | 
				
			||||||
    for i, name in ipairs(results) do
 | 
					 | 
				
			||||||
        local pending      = hgetall(name)
 | 
					 | 
				
			||||||
        local lastInterval = pending["interval"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if lastInterval == nil then
 | 
					 | 
				
			||||||
            lastInterval = 0
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        local nextInterval = getNextInterval(tonumber(lastInterval))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        redis.call("HSET", name, "interval", nextInterval)
 | 
					 | 
				
			||||||
        redis.call("ZADD", pendingNotificationQueue, tonumber(maxTime) + nextInterval, name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        collated[i] = pending["account"] .. ":" .. pending["device"]
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
return collated
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,14 +0,0 @@
 | 
				
			||||||
local pendingNotificationQueue = KEYS[1]
 | 
					 | 
				
			||||||
local endpoint = KEYS[2]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
local timestamp = ARGV[1]
 | 
					 | 
				
			||||||
local interval = ARGV[2]
 | 
					 | 
				
			||||||
local account = ARGV[3]
 | 
					 | 
				
			||||||
local deviceId = ARGV[4]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
redis.call("HSET", endpoint, "created", timestamp)
 | 
					 | 
				
			||||||
redis.call("HSET", endpoint, "interval", interval)
 | 
					 | 
				
			||||||
redis.call("HSET", endpoint, "account", account)
 | 
					 | 
				
			||||||
redis.call("HSET", endpoint, "device", deviceId)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
redis.call("ZADD", pendingNotificationQueue, timestamp, endpoint)
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
local pendingNotificationQueue = KEYS[1]
 | 
					 | 
				
			||||||
local endpoint = KEYS[2]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
redis.call("DEL", endpoint)
 | 
					 | 
				
			||||||
return redis.call("ZREM", pendingNotificationQueue, endpoint)
 | 
					 | 
				
			||||||
| 
						 | 
					@ -188,32 +188,6 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					 | 
				
			||||||
  @ValueSource(booleans = {true, false})
 | 
					 | 
				
			||||||
  void testSendNotificationApnVoip(final boolean urgent) {
 | 
					 | 
				
			||||||
    final Account account = mock(Account.class);
 | 
					 | 
				
			||||||
    final Device device = mock(Device.class);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(device.getId()).thenReturn(Device.PRIMARY_ID);
 | 
					 | 
				
			||||||
    when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(device));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final PushNotification pushNotification = new PushNotification(
 | 
					 | 
				
			||||||
        "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(apnSender.sendNotification(pushNotification))
 | 
					 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty())));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pushNotificationManager.sendNotification(pushNotification);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    verify(apnSender).sendNotification(pushNotification);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    verifyNoInteractions(fcmSender);
 | 
					 | 
				
			||||||
    verify(accountsManager, never()).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
 | 
					 | 
				
			||||||
    verify(device, never()).setGcmId(any());
 | 
					 | 
				
			||||||
    verify(pushNotificationScheduler).scheduleRecurringApnsVoipNotification(account, device);
 | 
					 | 
				
			||||||
    verify(pushNotificationScheduler, never()).scheduleBackgroundApnsNotification(any(), any());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSendNotificationUnregisteredFcm() {
 | 
					  void testSendNotificationUnregisteredFcm() {
 | 
				
			||||||
    final Account account = mock(Account.class);
 | 
					    final Account account = mock(Account.class);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.push;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
					import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
				
			||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
 | 
					import static org.junit.jupiter.api.Assertions.assertFalse;
 | 
				
			||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
 | 
					 | 
				
			||||||
import static org.mockito.ArgumentMatchers.any;
 | 
					import static org.mockito.ArgumentMatchers.any;
 | 
				
			||||||
import static org.mockito.Mockito.atLeastOnce;
 | 
					import static org.mockito.Mockito.atLeastOnce;
 | 
				
			||||||
import static org.mockito.Mockito.mock;
 | 
					import static org.mockito.Mockito.mock;
 | 
				
			||||||
| 
						 | 
					@ -20,7 +19,6 @@ import io.lettuce.core.cluster.SlotHash;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.time.temporal.ChronoUnit;
 | 
					import java.time.temporal.ChronoUnit;
 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
| 
						 | 
					@ -38,7 +36,6 @@ import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Account;
 | 
					import org.whispersystems.textsecuregcm.storage.Account;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
					import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device;
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Pair;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.TestClock;
 | 
					import org.whispersystems.textsecuregcm.util.TestClock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PushNotificationSchedulerTest {
 | 
					class PushNotificationSchedulerTest {
 | 
				
			||||||
| 
						 | 
					@ -95,57 +92,6 @@ class PushNotificationSchedulerTest {
 | 
				
			||||||
        apnSender, fcmSender, accountsManager, clock, 1, 1);
 | 
					        apnSender, fcmSender, accountsManager, clock, 1, 1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void testClusterInsert() throws ExecutionException, InterruptedException {
 | 
					 | 
				
			||||||
    final String endpoint = PushNotificationScheduler.getVoipEndpointKey(ACCOUNT_UUID, DEVICE_ID);
 | 
					 | 
				
			||||||
    final long currentTimeMillis = System.currentTimeMillis();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertTrue(
 | 
					 | 
				
			||||||
        pushNotificationScheduler.getPendingDestinationsForRecurringApnsVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000));
 | 
					 | 
				
			||||||
    pushNotificationScheduler.scheduleRecurringApnsVoipNotification(account, device).toCompletableFuture().get();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    clock.pin(Instant.ofEpochMilli(currentTimeMillis));
 | 
					 | 
				
			||||||
    final List<String> pendingDestinations = pushNotificationScheduler.getPendingDestinationsForRecurringApnsVoipNotifications(SlotHash.getSlot(endpoint), 2);
 | 
					 | 
				
			||||||
    assertEquals(1, pendingDestinations.size());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Pair<UUID, Byte> aciAndDeviceId =
 | 
					 | 
				
			||||||
        PushNotificationScheduler.decodeAciAndDeviceId(pendingDestinations.getFirst());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(ACCOUNT_UUID, aciAndDeviceId.first());
 | 
					 | 
				
			||||||
    assertEquals(DEVICE_ID, aciAndDeviceId.second());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertTrue(
 | 
					 | 
				
			||||||
        pushNotificationScheduler.getPendingDestinationsForRecurringApnsVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void testProcessRecurringVoipNotifications() throws ExecutionException, InterruptedException {
 | 
					 | 
				
			||||||
    final PushNotificationScheduler.NotificationWorker worker = pushNotificationScheduler.new NotificationWorker(1);
 | 
					 | 
				
			||||||
    final long currentTimeMillis = System.currentTimeMillis();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000));
 | 
					 | 
				
			||||||
    pushNotificationScheduler.scheduleRecurringApnsVoipNotification(account, device).toCompletableFuture().get();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    clock.pin(Instant.ofEpochMilli(currentTimeMillis));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final int slot = SlotHash.getSlot(PushNotificationScheduler.getVoipEndpointKey(ACCOUNT_UUID, DEVICE_ID));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(1, worker.processRecurringApnsVoipNotifications(slot));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
 | 
					 | 
				
			||||||
    verify(apnSender).sendNotification(notificationCaptor.capture());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final PushNotification pushNotification = notificationCaptor.getValue();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(VOIP_APN_ID, pushNotification.deviceToken());
 | 
					 | 
				
			||||||
    assertEquals(account, pushNotification.destination());
 | 
					 | 
				
			||||||
    assertEquals(device, pushNotification.destinationDevice());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(0, worker.processRecurringApnsVoipNotifications(slot));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testScheduleBackgroundNotificationWithNoRecentApnsNotification() throws ExecutionException, InterruptedException {
 | 
					  void testScheduleBackgroundNotificationWithNoRecentApnsNotification() throws ExecutionException, InterruptedException {
 | 
				
			||||||
    final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
 | 
					    final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
 | 
				
			||||||
| 
						 | 
					@ -228,8 +174,6 @@ class PushNotificationSchedulerTest {
 | 
				
			||||||
    assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType());
 | 
					    assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType());
 | 
				
			||||||
    assertFalse(pushNotification.urgent());
 | 
					    assertFalse(pushNotification.urgent());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertEquals(0, worker.processRecurringApnsVoipNotifications(slot));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(Optional.empty(),
 | 
					    assertEquals(Optional.empty(),
 | 
				
			||||||
        pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
 | 
					        pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue