public CompletableFuture<Void> reserveUsernameHash(
      final Account account,
      final byte[] reservedUsernameHash,
      final Duration ttl) {

    final Timer.Sample sample = Timer.start();

    // if there is an existing old reservation it will be cleaned up via ttl. Save it so we can restore it to the local
    // account if the update fails though.
    final Optional<byte[]> maybeOriginalReservation = account.getReservedUsernameHash();
    account.setReservedUsernameHash(reservedUsernameHash);

    // Normally when a username is reserved for the first time we reserve it for the provided TTL. But if the
    // reservation is for a username that we already have a reservation for (for example, if it's reclaimable, or there
    // is a hold) we might own that reservation for longer anyways, so we should preserve the original TTL in that case.
    // What we'd really like to do is set expirationTime = max(oldExpirationTime, now + ttl), but dynamodb doesn't
    // support that. Instead, we'll set expiration if it's greater than the existing expiration, otherwise retry
    final long expirationTime = clock.instant().plus(ttl).getEpochSecond();
    return tryReserveUsernameHash(account, reservedUsernameHash, expirationTime)
        .exceptionallyCompose(ExceptionUtils.exceptionallyHandler(TtlConflictException.class, ttlConflict ->
            // retry (once) with the returned expiration time
            tryReserveUsernameHash(account, reservedUsernameHash, ttlConflict.getExistingExpirationSeconds())))
        .whenComplete((response, throwable) -> {
          sample.stop(RESERVE_USERNAME_TIMER);

          if (throwable == null) {
            account.setVersion(account.getVersion() + 1);
          } else {
            account.setReservedUsernameHash(maybeOriginalReservation.orElse(null));
          }
        });
  }
@Test
  void switchBetweenReservedUsernameHashes() {
    final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
    createAccount(account);

    accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
    assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
    assertThat(account.getUsernameHash()).isEmpty();

    accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1)).join();
    assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_2);
    assertThat(account.getUsernameHash()).isEmpty();

    final Map<String, AttributeValue> usernameConstraintRecord1 = getUsernameConstraintTableItem(USERNAME_HASH_1);
    final Map<String, AttributeValue> usernameConstraintRecord2 = getUsernameConstraintTableItem(USERNAME_HASH_2);
    assertThat(usernameConstraintRecord1).containsKey(Accounts.UsernameTable.KEY_USERNAME_HASH);
    assertThat(usernameConstraintRecord2).containsKey(Accounts.UsernameTable.KEY_USERNAME_HASH);
    assertThat(usernameConstraintRecord1).containsKey(Accounts.UsernameTable.ATTR_TTL);
    assertThat(usernameConstraintRecord2).containsKey(Accounts.UsernameTable.ATTR_TTL);

    clock.pin(Instant.EPOCH.plus(Duration.ofMinutes(1)));

    accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
    assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
    assertThat(account.getUsernameHash()).isEmpty();

    final Map<String, AttributeValue> newUsernameConstraintRecord1 = getUsernameConstraintTableItem(USERNAME_HASH_1);
    assertThat(newUsernameConstraintRecord1).containsKey(Accounts.UsernameTable.KEY_USERNAME_HASH);
    assertThat(newUsernameConstraintRecord1).containsKey(Accounts.UsernameTable.ATTR_TTL);
    assertThat(usernameConstraintRecord1.get(Accounts.UsernameTable.ATTR_TTL))
        .isNotEqualTo(newUsernameConstraintRecord1.get(Accounts.UsernameTable.ATTR_TTL));
  }