Reduce contention when updating `device.lastSeen`
This commit is contained in:
		
							parent
							
								
									13a07dc6cd
								
							
						
					
					
						commit
						d45659ac76
					
				| 
						 | 
					@ -83,7 +83,8 @@ public class BaseAccountAuthenticator {
 | 
				
			||||||
      if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
 | 
					      if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
 | 
				
			||||||
        succeeded = true;
 | 
					        succeeded = true;
 | 
				
			||||||
        final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
 | 
					        final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
 | 
				
			||||||
        authenticatedAccount.setAuthenticatedDevice(device.get());
 | 
					        // the device in scope might be stale after the update, so get the latest from the authenticated account
 | 
				
			||||||
 | 
					        authenticatedAccount.setAuthenticatedDevice(authenticatedAccount.getDevice(device.get().getId()).orElseThrow());
 | 
				
			||||||
        return Optional.of(authenticatedAccount);
 | 
					        return Optional.of(authenticatedAccount);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,7 +118,7 @@ public class BaseAccountAuthenticator {
 | 
				
			||||||
      Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
 | 
					      Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
 | 
				
			||||||
          .record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
 | 
					          .record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return accountsManager.updateDevice(account, device.getId(), d -> d.setLastSeen(Util.todayInMillis(clock)));
 | 
					      return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return account;
 | 
					    return account;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ import java.util.concurrent.Callable;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
import java.util.function.BiFunction;
 | 
					import java.util.function.BiFunction;
 | 
				
			||||||
import java.util.function.Consumer;
 | 
					import java.util.function.Consumer;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
import java.util.function.Supplier;
 | 
					import java.util.function.Supplier;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
import net.logstash.logback.argument.StructuredArguments;
 | 
					import net.logstash.logback.argument.StructuredArguments;
 | 
				
			||||||
| 
						 | 
					@ -250,12 +251,51 @@ public class AccountsManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Account update(Account account, Consumer<Account> updater) {
 | 
					  public Account update(Account account, Consumer<Account> updater) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return update(account, a -> {
 | 
				
			||||||
 | 
					      updater.accept(a);
 | 
				
			||||||
 | 
					      // assume that all updaters passed to the public method actually modify the account
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Specialized version of {@link #updateDevice(Account, long, Consumer)} that minimizes potentially contentious and
 | 
				
			||||||
 | 
					   * redundant updates of {@code device.lastSeen}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public Account updateDeviceLastSeen(Account account, Device device, final long lastSeen) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return update(account, a -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final Optional<Device> maybeDevice = a.getDevice(device.getId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return maybeDevice.map(d -> {
 | 
				
			||||||
 | 
					        if (d.getLastSeen() >= lastSeen) {
 | 
				
			||||||
 | 
					          return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        d.setLastSeen(lastSeen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      }).orElse(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param account account to update
 | 
				
			||||||
 | 
					   * @param updater must return {@code true} if the account was actually updated
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private Account update(Account account, Function<Account, Boolean> updater) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final boolean wasVisibleBeforeUpdate = account.shouldBeVisibleInDirectory();
 | 
					    final boolean wasVisibleBeforeUpdate = account.shouldBeVisibleInDirectory();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final Account updatedAccount;
 | 
					    final Account updatedAccount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try (Timer.Context ignored = updateTimer.time()) {
 | 
					    try (Timer.Context ignored = updateTimer.time()) {
 | 
				
			||||||
      updater.accept(account);
 | 
					
 | 
				
			||||||
 | 
					      if (!updater.apply(account)) {
 | 
				
			||||||
 | 
					        return account;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        // optimistically increment version
 | 
					        // optimistically increment version
 | 
				
			||||||
| 
						 | 
					@ -274,7 +314,11 @@ public class AccountsManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              final Optional<Account> dynamoAccount = dynamoGet(uuid);
 | 
					              final Optional<Account> dynamoAccount = dynamoGet(uuid);
 | 
				
			||||||
              if (dynamoAccount.isPresent()) {
 | 
					              if (dynamoAccount.isPresent()) {
 | 
				
			||||||
                updater.accept(dynamoAccount.get());
 | 
					
 | 
				
			||||||
 | 
					                if (!updater.apply(dynamoAccount.get())) {
 | 
				
			||||||
 | 
					                  return dynamoAccount;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Account dynamoUpdatedAccount = updateWithRetries(dynamoAccount.get(),
 | 
					                Account dynamoUpdatedAccount = updateWithRetries(dynamoAccount.get(),
 | 
				
			||||||
                    updater,
 | 
					                    updater,
 | 
				
			||||||
                    this::dynamoUpdate,
 | 
					                    this::dynamoUpdate,
 | 
				
			||||||
| 
						 | 
					@ -302,7 +346,8 @@ public class AccountsManager {
 | 
				
			||||||
    return updatedAccount;
 | 
					    return updatedAccount;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private Account updateWithRetries(Account account, Consumer<Account> updater, Consumer<Account> persister, Supplier<Account> retriever) {
 | 
					  private Account updateWithRetries(Account account, Function<Account, Boolean> updater, Consumer<Account> persister,
 | 
				
			||||||
 | 
					      Supplier<Account> retriever) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final int maxTries = 10;
 | 
					    final int maxTries = 10;
 | 
				
			||||||
    int tries = 0;
 | 
					    int tries = 0;
 | 
				
			||||||
| 
						 | 
					@ -327,7 +372,10 @@ public class AccountsManager {
 | 
				
			||||||
      } catch (final ContestedOptimisticLockException e) {
 | 
					      } catch (final ContestedOptimisticLockException e) {
 | 
				
			||||||
        tries++;
 | 
					        tries++;
 | 
				
			||||||
        account = retriever.get();
 | 
					        account = retriever.get();
 | 
				
			||||||
        updater.accept(account);
 | 
					
 | 
				
			||||||
 | 
					        if (!updater.apply(account)) {
 | 
				
			||||||
 | 
					          return account;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -335,7 +383,11 @@ public class AccountsManager {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Account updateDevice(Account account, long deviceId, Consumer<Device> deviceUpdater) {
 | 
					  public Account updateDevice(Account account, long deviceId, Consumer<Device> deviceUpdater) {
 | 
				
			||||||
    return update(account, a -> a.getDevice(deviceId).ifPresent(deviceUpdater));
 | 
					    return update(account, a -> {
 | 
				
			||||||
 | 
					      a.getDevice(deviceId).ifPresent(deviceUpdater);
 | 
				
			||||||
 | 
					      // assume that all updaters passed to the public method actually modify the device
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Optional<Account> get(AmbiguousIdentifier identifier) {
 | 
					  public Optional<Account> get(AmbiguousIdentifier identifier) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,8 +68,8 @@ class BaseAccountAuthenticatorTest {
 | 
				
			||||||
    final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1);
 | 
					    final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1);
 | 
				
			||||||
    final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2);
 | 
					    final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(accountsManager, never()).updateDevice(eq(acct1), anyLong(), any());
 | 
					    verify(accountsManager, never()).updateDeviceLastSeen(eq(acct1), any(), anyLong());
 | 
				
			||||||
    verify(accountsManager).updateDevice(eq(acct2), anyLong(), any());
 | 
					    verify(accountsManager).updateDeviceLastSeen(eq(acct2), eq(device2), anyLong());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(device1.getLastSeen()).isEqualTo(yesterday);
 | 
					    assertThat(device1.getLastSeen()).isEqualTo(yesterday);
 | 
				
			||||||
    assertThat(device2.getLastSeen()).isEqualTo(today);
 | 
					    assertThat(device2.getLastSeen()).isEqualTo(today);
 | 
				
			||||||
| 
						 | 
					@ -88,8 +88,8 @@ class BaseAccountAuthenticatorTest {
 | 
				
			||||||
    final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1);
 | 
					    final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1);
 | 
				
			||||||
    final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2);
 | 
					    final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(accountsManager, never()).updateDevice(eq(acct1), anyLong(), any());
 | 
					    verify(accountsManager, never()).updateDeviceLastSeen(eq(acct1), any(), anyLong());
 | 
				
			||||||
    verify(accountsManager, never()).updateDevice(eq(acct2), anyLong(), any());
 | 
					    verify(accountsManager, never()).updateDeviceLastSeen(eq(acct2), any(), anyLong());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(device1.getLastSeen()).isEqualTo(yesterday);
 | 
					    assertThat(device1.getLastSeen()).isEqualTo(yesterday);
 | 
				
			||||||
    assertThat(device2.getLastSeen()).isEqualTo(yesterday);
 | 
					    assertThat(device2.getLastSeen()).isEqualTo(yesterday);
 | 
				
			||||||
| 
						 | 
					@ -108,8 +108,8 @@ class BaseAccountAuthenticatorTest {
 | 
				
			||||||
    final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1);
 | 
					    final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1);
 | 
				
			||||||
    final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2);
 | 
					    final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(accountsManager).updateDevice(eq(acct1), anyLong(), any());
 | 
					    verify(accountsManager).updateDeviceLastSeen(eq(acct1), eq(device1), anyLong());
 | 
				
			||||||
    verify(accountsManager).updateDevice(eq(acct2), anyLong(), any());
 | 
					    verify(accountsManager).updateDeviceLastSeen(eq(acct2), eq(device2), anyLong());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(device1.getLastSeen()).isEqualTo(today);
 | 
					    assertThat(device1.getLastSeen()).isEqualTo(today);
 | 
				
			||||||
    assertThat(device2.getLastSeen()).isEqualTo(today);
 | 
					    assertThat(device2.getLastSeen()).isEqualTo(today);
 | 
				
			||||||
| 
						 | 
					@ -126,7 +126,7 @@ class BaseAccountAuthenticatorTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    baseAccountAuthenticator.updateLastSeen(oldAccount, device);
 | 
					    baseAccountAuthenticator.updateLastSeen(oldAccount, device);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(accountsManager).updateDevice(eq(oldAccount), anyLong(), any());
 | 
					    verify(accountsManager).updateDeviceLastSeen(eq(oldAccount), eq(device), anyLong());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(device.getLastSeen()).isEqualTo(today);
 | 
					    assertThat(device.getLastSeen()).isEqualTo(today);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -641,4 +641,28 @@ class AccountsManagerTest {
 | 
				
			||||||
        Arguments.of(false, true, true),
 | 
					        Arguments.of(false, true, true),
 | 
				
			||||||
        Arguments.of(true, false, true));
 | 
					        Arguments.of(true, false, true));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ParameterizedTest
 | 
				
			||||||
 | 
					  @MethodSource
 | 
				
			||||||
 | 
					  void testUpdateDeviceLastSeen(final boolean expectUpdate, final long initialLastSeen, final long updatedLastSeen) {
 | 
				
			||||||
 | 
					    final Account account = new Account("+14152222222", UUID.randomUUID(), new HashSet<>(), new byte[16]);
 | 
				
			||||||
 | 
					    final Device device = new Device(Device.MASTER_ID, "device", "token", "salt", null, null, null, true, 1,
 | 
				
			||||||
 | 
					        new SignedPreKey(1, "key", "sig"), initialLastSeen, 0,
 | 
				
			||||||
 | 
					        "OWT", 0, new DeviceCapabilities());
 | 
				
			||||||
 | 
					    account.addDevice(device);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    accountsManager.updateDeviceLastSeen(account, device, updatedLastSeen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertEquals(expectUpdate ? updatedLastSeen : initialLastSeen, device.getLastSeen());
 | 
				
			||||||
 | 
					    verify(accounts, expectUpdate ? times(1) : never()).update(account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @SuppressWarnings("unused")
 | 
				
			||||||
 | 
					  private static Stream<Arguments> testUpdateDeviceLastSeen() {
 | 
				
			||||||
 | 
					    return Stream.of(
 | 
				
			||||||
 | 
					        Arguments.of(true, 1, 2),
 | 
				
			||||||
 | 
					        Arguments.of(false, 1, 1),
 | 
				
			||||||
 | 
					        Arguments.of(false, 2, 1)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ import org.mockito.MockingDetails;
 | 
				
			||||||
import org.mockito.stubbing.Stubbing;
 | 
					import org.mockito.stubbing.Stubbing;
 | 
				
			||||||
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.util.SystemMapper;
 | 
					import org.whispersystems.textsecuregcm.util.SystemMapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class AccountsHelper {
 | 
					public class AccountsHelper {
 | 
				
			||||||
| 
						 | 
					@ -51,6 +52,13 @@ public class AccountsHelper {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return markStale ? copyAndMarkStale(account) : account;
 | 
					      return markStale ? copyAndMarkStale(account) : account;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(mockAccountsManager.updateDeviceLastSeen(any(), any(), anyLong())).thenAnswer(answer -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      answer.getArgument(1, Device.class).setLastSeen(answer.getArgument(2, Long.class));
 | 
				
			||||||
 | 
					      return mockAccountsManager.update(answer.getArgument(0, Account.class), account -> {
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static void setupMockGet(final AccountsManager mockAccountsManager, final Set<Account> mockAccounts) {
 | 
					  public static void setupMockGet(final AccountsManager mockAccountsManager, final Set<Account> mockAccounts) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue