public synchronized void rescan() {
    membershipQueue.clear();
    Date now = new Date();
    for (Player player : Bukkit.getOnlinePlayers()) {
      for (Membership membership :
          storageStrategy
              .getPermissionService()
              .getGroups(UUIDProvider.retrieve(player.getName()))) {
        if (membership.getExpiration() != null && membership.getExpiration().after(now)) {
          membershipQueue.add(membership);
        }
      }
    }

    debug(plugin, "Potential future expirations: %s", membershipQueue);

    // Queue up task
    run();
  }
  @Override
  public synchronized void run() {
    Set<UUID> toRefresh = new LinkedHashSet<>();
    final Set<Membership> expired = new LinkedHashSet<>();

    // Gather up memberships that have already expired
    Date now = new Date();
    Membership next = membershipQueue.peek();
    while (next != null && !next.getExpiration().after(now)) {
      membershipQueue.remove();

      toRefresh.add(next.getUuid());
      expired.add(next);

      now = new Date();
      next = membershipQueue.peek();
    }

    debug(plugin, "Refreshing expired players: %s", toRefresh);
    // NB Metadata cache for offline players not invalidated.
    // This might become a problem. But nothing can be done unless we
    // run a timer for each and every membership, online or offline.
    core.refreshPlayers(toRefresh);

    // Send notifications
    if (!expired.isEmpty()) {
      Bukkit.getScheduler()
          .scheduleSyncDelayedTask(
              plugin,
              new Runnable() {
                @Override
                public void run() {
                  for (Membership membership : expired) {
                    Player player = Bukkit.getPlayer(UUIDProvider.retrieve(membership.getUuid()));
                    if (player != null
                        && player.hasPermission("zpermissions.notify.self.expiration")) {
                      sendMessage(
                          player,
                          colorize(
                              "{YELLOW}Your membership to {DARK_GREEN}%s{YELLOW} has expired."),
                          membership.getGroup().getDisplayName());
                    }
                    broadcast(
                        plugin,
                        "zpermissions.notify.expiration",
                        "Player %s is no longer a member of %s",
                        membership.getDisplayName(),
                        membership.getGroup().getDisplayName());
                  }
                }
              });
    }

    // Cancel previous task
    if (scheduledFuture != null) {
      scheduledFuture.cancel(false);
      scheduledFuture = null;
    }

    // Schedule new task
    if (next != null) {
      now = new Date();
      long delay = next.getExpiration().getTime() - now.getTime();

      if (delay < 0L) delay = 0L; // Weird...

      debug(plugin, "Next expiration is %dms away", delay);

      final Runnable realThis = this;
      scheduledFuture =
          executorService.schedule(
              new Runnable() {
                @Override
                public void run() {
                  debug(plugin, "Expiring...");
                  Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, realThis);
                }
              },
              delay + FUDGE,
              TimeUnit.MILLISECONDS);
    } else debug(plugin, "No future expirations");
  }
 @Override
 public int compare(Membership a, Membership b) {
   return a.getExpiration().compareTo(b.getExpiration());
 }