Use `ClientReleasesManager` when deciding whether to add client version tags

This commit is contained in:
Jon Chambers 2023-07-25 18:17:03 -04:00 committed by Jon Chambers
parent 6f4801fd6f
commit 4ead8527c8
19 changed files with 117 additions and 164 deletions

View File

@ -88,6 +88,8 @@ dynamoDbTables:
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
usernamesTableName: Example_Accounts_Usernames
scanPageSize: 100
clientReleases:
tableName: Example_ClientReleases
deletedAccounts:
tableName: Example_DeletedAccounts
deletedAccountsLock:

View File

@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@ -24,6 +25,7 @@ import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguratio
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
@ -281,6 +283,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private int grpcPort;
@Valid
@NotNull
@JsonProperty
private ClientReleaseConfiguration clientRelease = new ClientReleaseConfiguration(Duration.ofHours(4));
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
return adminEventLoggingConfiguration;
}
@ -468,4 +475,7 @@ public class WhisperServerConfiguration extends Configuration {
return grpcPort;
}
public ClientReleaseConfiguration getClientReleaseConfiguration() {
return clientRelease;
}
}

View File

@ -168,6 +168,8 @@ import org.whispersystems.textsecuregcm.storage.AccountLockManager;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.ClientReleases;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
@ -320,6 +322,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(),
config.getDynamoDbTables().getAccounts().getUsernamesTableName(),
config.getDynamoDbTables().getAccounts().getScanPageSize());
ClientReleases clientReleases = new ClientReleases(dynamoDbAsyncClient,
config.getDynamoDbTables().getClientReleases().getTableName());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient,
config.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName());
Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient,
@ -499,7 +503,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster,
keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionAsyncExecutor, clock);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
ClientReleaseManager clientReleaseManager = new ClientReleaseManager(clientReleases,
recurringJobExecutor,
config.getClientReleaseConfiguration().refreshInterval(),
Clock.systemUTC());
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, clientReleaseManager);
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster,
config.getReportMessageConfiguration().getCounterTtl());
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
@ -587,6 +595,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(clientPresenceManager);
environment.lifecycle().manage(currencyManager);
environment.lifecycle().manage(registrationServiceClient);
environment.lifecycle().manage(clientReleaseManager);
final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker,
rateLimiters, config.getTestDevices(), dynamicConfigurationManager);
@ -662,7 +671,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
webSocketEnvironment.setConnectListener(
new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager,
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, dynamicConfigurationManager));
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, clientReleaseManager));
webSocketEnvironment.jersey()
.register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
webSocketEnvironment.jersey().register(new RequestStatisticsFilter(TrafficSource.WEBSOCKET));
@ -740,7 +749,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ReceiptCredentialPresentation::new),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, reportSpamTokenProvider, dynamicConfigurationManager),
messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager, dynamicConfigurationManager),
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,

View File

@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotNull;
import java.time.Duration;
public record ClientReleaseConfiguration(@NotNull Duration refreshInterval) {
}

View File

@ -47,6 +47,7 @@ public class DynamoDbTables {
}
private final AccountsTableConfiguration accounts;
private final Table clientReleases;
private final Table deletedAccounts;
private final Table deletedAccountsLock;
private final IssuedReceiptsTableConfiguration issuedReceipts;
@ -69,6 +70,7 @@ public class DynamoDbTables {
public DynamoDbTables(
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
@JsonProperty("clientReleases") final Table clientReleases,
@JsonProperty("deletedAccounts") final Table deletedAccounts,
@JsonProperty("deletedAccountsLock") final Table deletedAccountsLock,
@JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts,
@ -90,6 +92,7 @@ public class DynamoDbTables {
@JsonProperty("verificationSessions") final Table verificationSessions) {
this.accounts = accounts;
this.clientReleases = clientReleases;
this.deletedAccounts = deletedAccounts;
this.deletedAccountsLock = deletedAccountsLock;
this.issuedReceipts = issuedReceipts;
@ -117,6 +120,12 @@ public class DynamoDbTables {
return accounts;
}
@NotNull
@Valid
public Table getClientReleases() {
return clientReleases;
}
@NotNull
@Valid
public Table getDeletedAccounts() {

View File

@ -39,10 +39,6 @@ public class DynamicConfiguration {
@Valid
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
@JsonProperty
@Valid
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
@JsonProperty
@Valid
private DynamicTurnConfiguration turn = new DynamicTurnConfiguration();
@ -59,10 +55,6 @@ public class DynamicConfiguration {
@Valid
DynamicECPreKeyMigrationConfiguration ecPreKeyMigration = new DynamicECPreKeyMigrationConfiguration(true, false);
@JsonProperty
@Valid
DynamicDeliveryLatencyConfiguration deliveryLatency = new DynamicDeliveryLatencyConfiguration(Collections.emptyMap());
@JsonProperty
@Valid
DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true);
@ -93,10 +85,6 @@ public class DynamicConfiguration {
return captcha;
}
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
return pushLatency;
}
public DynamicTurnConfiguration getTurnConfiguration() {
return turn;
}
@ -113,10 +101,6 @@ public class DynamicConfiguration {
return ecPreKeyMigration;
}
public DynamicDeliveryLatencyConfiguration getDeliveryLatencyConfiguration() {
return deliveryLatency;
}
public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() {
return inboundMessageByteLimit;
}

View File

@ -1,14 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import java.util.Map;
import java.util.Set;
public record DynamicDeliveryLatencyConfiguration(Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
}

View File

@ -1,16 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import java.util.Map;
import java.util.Set;
public record DynamicPushLatencyConfiguration(Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
}

View File

@ -97,6 +97,7 @@ import org.whispersystems.textsecuregcm.spam.FilterSpam;
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
@ -127,6 +128,7 @@ public class MessageController {
private final ExecutorService multiRecipientMessageExecutor;
private final Scheduler messageDeliveryScheduler;
private final ReportSpamTokenProvider reportSpamTokenProvider;
private final ClientReleaseManager clientReleaseManager;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "rejectOversizeMessage");
@ -161,6 +163,7 @@ public class MessageController {
@Nonnull ExecutorService multiRecipientMessageExecutor,
Scheduler messageDeliveryScheduler,
@Nonnull ReportSpamTokenProvider reportSpamTokenProvider,
final ClientReleaseManager clientReleaseManager,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
this.rateLimiters = rateLimiters;
this.messageSender = messageSender;
@ -173,6 +176,7 @@ public class MessageController {
this.multiRecipientMessageExecutor = Objects.requireNonNull(multiRecipientMessageExecutor);
this.messageDeliveryScheduler = messageDeliveryScheduler;
this.reportSpamTokenProvider = reportSpamTokenProvider;
this.clientReleaseManager = clientReleaseManager;
this.dynamicConfigurationManager = dynamicConfigurationManager;
}
@ -543,8 +547,7 @@ public class MessageController {
.map(OutgoingMessageEntity::fromEnvelope)
.peek(outgoingMessageEntity -> {
MessageMetrics.measureAccountOutgoingMessageUuidMismatches(auth.getAccount(), outgoingMessageEntity);
MessageMetrics.measureOutgoingMessageLatency(outgoingMessageEntity.serverTimestamp(), "rest", userAgent,
dynamicConfigurationManager.getConfiguration().getDeliveryLatencyConfiguration().instrumentedVersions());
MessageMetrics.measureOutgoingMessageLatency(outgoingMessageEntity.serverTimestamp(), "rest", userAgent, clientReleaseManager);
})
.collect(Collectors.toList()),
messagesAndHasMore.second());

View File

@ -22,6 +22,7 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public final class MessageMetrics {
@ -61,13 +62,13 @@ public final class MessageMetrics {
public static void measureOutgoingMessageLatency(final long serverTimestamp,
final String channel,
final String userAgent,
final Map<ClientPlatform, Set<Semver>> taggedVersions) {
final ClientReleaseManager clientReleaseManager) {
final List<Tag> tags = new ArrayList<>(3);
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
tags.add(Tag.of("channel", channel));
UserAgentTagUtil.getClientVersionTag(userAgent, taggedVersions).ifPresent(tags::add);
UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager).ifPresent(tags::add);
Metrics.timer(DELIVERY_LATENCY_TIMER_NAME, tags)
.record(Duration.between(Instant.ofEpochMilli(serverTimestamp), Instant.now()));

View File

@ -11,6 +11,7 @@ import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
@ -39,11 +40,11 @@ public class UserAgentTagUtil {
return Tag.of(PLATFORM_TAG, platform);
}
public static Optional<Tag> getClientVersionTag(final String userAgentString, final Map<ClientPlatform, Set<Semver>> taggedVersions) {
public static Optional<Tag> getClientVersionTag(final String userAgentString, final ClientReleaseManager clientReleaseManager) {
try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
if (taggedVersions.getOrDefault(userAgent.getPlatform(), Collections.emptySet()).contains(userAgent.getVersion())) {
if (clientReleaseManager.isVersionActive(userAgent.getPlatform(), userAgent.getVersion())) {
return Optional.of(Tag.of(VERSION_TAG, userAgent.getVersion().toString()));
}
} catch (final UnrecognizedUserAgentException ignored) {

View File

@ -22,10 +22,9 @@ import java.util.concurrent.CompletableFuture;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.SystemMapper;
/**
@ -40,7 +39,7 @@ import org.whispersystems.textsecuregcm.util.SystemMapper;
public class PushLatencyManager {
private final FaultTolerantRedisCluster redisCluster;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final ClientReleaseManager clientReleaseManager;
private final Clock clock;
@ -59,18 +58,18 @@ public class PushLatencyManager {
}
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
final ClientReleaseManager clientReleaseManager) {
this(redisCluster, dynamicConfigurationManager, Clock.systemUTC());
this(redisCluster, clientReleaseManager, Clock.systemUTC());
}
@VisibleForTesting
PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final ClientReleaseManager clientReleaseManager,
final Clock clock) {
this.redisCluster = redisCluster;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.clientReleaseManager = clientReleaseManager;
this.clock = clock;
}
@ -99,8 +98,7 @@ public class PushLatencyManager {
tags.add(UserAgentTagUtil.getPlatformTag(userAgentString));
tags.add(Tag.of("pushType", pushRecord.pushType().name().toLowerCase()));
UserAgentTagUtil.getClientVersionTag(userAgentString,
dynamicConfigurationManager.getConfiguration().getPushLatencyConfiguration().instrumentedVersions())
UserAgentTagUtil.getClientVersionTag(userAgentString, clientReleaseManager)
.ifPresent(tags::add);
pushRecord.urgent().ifPresent(urgent -> tags.add(Tag.of("urgent", String.valueOf(urgent))));

View File

@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
@ -31,8 +30,8 @@ import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.redis.RedisOperation;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
@ -69,7 +68,7 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
private final ClientPresenceManager clientPresenceManager;
private final ScheduledExecutorService scheduledExecutorService;
private final Scheduler messageDeliveryScheduler;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final ClientReleaseManager clientReleaseManager;
private final Map<ClientPlatform, AtomicInteger> openAuthenticatedWebsocketsByClientPlatform;
private final Map<ClientPlatform, AtomicInteger> openUnauthenticatedWebsocketsByClientPlatform;
@ -87,14 +86,14 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
ClientPresenceManager clientPresenceManager,
ScheduledExecutorService scheduledExecutorService,
Scheduler messageDeliveryScheduler,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
ClientReleaseManager clientReleaseManager) {
this.receiptSender = receiptSender;
this.messagesManager = messagesManager;
this.pushNotificationManager = pushNotificationManager;
this.clientPresenceManager = clientPresenceManager;
this.scheduledExecutorService = scheduledExecutorService;
this.messageDeliveryScheduler = messageDeliveryScheduler;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.clientReleaseManager = clientReleaseManager;
openAuthenticatedWebsocketsByClientPlatform = new EnumMap<>(ClientPlatform.class);
openUnauthenticatedWebsocketsByClientPlatform = new EnumMap<>(ClientPlatform.class);
@ -157,7 +156,7 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
context.getClient(),
scheduledExecutorService,
messageDeliveryScheduler,
dynamicConfigurationManager);
clientReleaseManager);
openWebsocketAtomicInteger.incrementAndGet();
openWebsocketCounter.inc();

View File

@ -37,7 +37,6 @@ import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
@ -47,8 +46,8 @@ import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.push.DisplacedPresenceListener;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessageAvailabilityListener;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.util.Constants;
@ -132,7 +131,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
private final Random random = new Random();
private final Scheduler messageDeliveryScheduler;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final ClientReleaseManager clientReleaseManager;
private enum StoredMessageState {
EMPTY,
@ -147,7 +146,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
WebSocketClient client,
ScheduledExecutorService scheduledExecutorService,
Scheduler messageDeliveryScheduler,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
ClientReleaseManager clientReleaseManager) {
this(receiptSender,
messagesManager,
@ -156,7 +155,8 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
client,
DEFAULT_SEND_FUTURES_TIMEOUT_MILLIS,
scheduledExecutorService,
messageDeliveryScheduler, dynamicConfigurationManager);
messageDeliveryScheduler,
clientReleaseManager);
}
@VisibleForTesting
@ -168,7 +168,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
int sendFuturesTimeoutMillis,
ScheduledExecutorService scheduledExecutorService,
Scheduler messageDeliveryScheduler,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
ClientReleaseManager clientReleaseManager) {
this.receiptSender = receiptSender;
this.messagesManager = messagesManager;
@ -178,7 +178,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
this.sendFuturesTimeoutMillis = sendFuturesTimeoutMillis;
this.scheduledExecutorService = scheduledExecutorService;
this.messageDeliveryScheduler = messageDeliveryScheduler;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.clientReleaseManager = clientReleaseManager;
}
public void start() {
@ -217,8 +217,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
if (throwable != null) {
sendFailuresMeter.mark();
} else {
MessageMetrics.measureOutgoingMessageLatency(message.getServerTimestamp(), "websocket", client.getUserAgent(),
dynamicConfigurationManager.getConfiguration().getDeliveryLatencyConfiguration().instrumentedVersions());
MessageMetrics.measureOutgoingMessageLatency(message.getServerTimestamp(), "websocket", client.getUserAgent(), clientReleaseManager);
}
}).thenCompose(response -> {
final CompletableFuture<Void> result;

View File

@ -81,7 +81,6 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicDeliveryLatencyConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicInboundMessageByteLimitConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountMismatchedDevices;
import org.whispersystems.textsecuregcm.entities.AccountStaleDevices;
@ -112,6 +111,7 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
@ -179,7 +179,7 @@ class MessageControllerTest {
.addResource(
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, ReportSpamTokenProvider.noop(), dynamicConfigurationManager))
messageDeliveryScheduler, ReportSpamTokenProvider.noop(), mock(ClientReleaseManager.class), dynamicConfigurationManager))
.build();
@BeforeEach
@ -209,16 +209,12 @@ class MessageControllerTest {
when(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(MULTI_DEVICE_PNI))).thenReturn(Optional.of(multiDeviceAccount));
when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(INTERNATIONAL_UUID))).thenReturn(Optional.of(internationalAccount));
final DynamicDeliveryLatencyConfiguration deliveryLatencyConfiguration = mock(DynamicDeliveryLatencyConfiguration.class);
when(deliveryLatencyConfiguration.instrumentedVersions()).thenReturn(Collections.emptyMap());
final DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimitConfiguration =
mock(DynamicInboundMessageByteLimitConfiguration.class);
when(inboundMessageByteLimitConfiguration.enforceInboundLimit()).thenReturn(false);
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfiguration.getDeliveryLatencyConfiguration()).thenReturn(deliveryLatencyConfiguration);
when(dynamicConfiguration.getInboundMessageByteLimitConfiguration()).thenReturn(inboundMessageByteLimitConfiguration);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);

View File

@ -6,6 +6,9 @@
package org.whispersystems.textsecuregcm.metrics;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.vdurmont.semver4j.Semver;
import io.micrometer.core.instrument.Tag;
@ -17,6 +20,7 @@ import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
class UserAgentTagUtilTest {
@ -49,26 +53,21 @@ class UserAgentTagUtilTest {
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@ParameterizedTest
@MethodSource
void getClientVersionTag(final String userAgent, final Map<ClientPlatform, Set<Semver>> taggedVersions, final Optional<Tag> expectedTag) {
assertEquals(expectedTag, UserAgentTagUtil.getClientVersionTag(userAgent, taggedVersions));
void getClientVersionTag(final String userAgent, final boolean isVersionLive, final Optional<Tag> expectedTag) {
final ClientReleaseManager clientReleaseManager = mock(ClientReleaseManager.class);
when(clientReleaseManager.isVersionActive(any(), any())).thenReturn(isVersionLive);
assertEquals(expectedTag, UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager));
}
private static Stream<Arguments> getClientVersionTag() {
return Stream.of(
Arguments.of("Signal-Android/1.2.3 (Android 9)",
Map.of(ClientPlatform.ANDROID, Set.of(new Semver("1.2.3"))),
true,
Optional.of(Tag.of(UserAgentTagUtil.VERSION_TAG, "1.2.3"))),
Arguments.of("Signal-Android/1.2.3 (Android 9)",
Collections.emptyMap(),
Optional.empty()),
Arguments.of("Signal-Android/1.2.3.0-bobsbootlegclient",
Map.of(ClientPlatform.ANDROID, Set.of(new Semver("1.2.3"))),
Optional.empty()),
Arguments.of("Signal-Desktop/1.2.3",
Map.of(ClientPlatform.ANDROID, Set.of(new Semver("1.2.3"))),
false,
Optional.empty())
);
}

View File

@ -9,47 +9,28 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPushLatencyConfiguration;
import org.whispersystems.textsecuregcm.push.PushLatencyManager.PushRecord;
import org.whispersystems.textsecuregcm.push.PushLatencyManager.PushType;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
class PushLatencyManagerTest {
@RegisterExtension
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
@BeforeEach
void setUp() {
//noinspection unchecked
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
final DynamicPushLatencyConfiguration dynamicPushLatencyConfiguration = mock(DynamicPushLatencyConfiguration.class);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
when(dynamicConfiguration.getPushLatencyConfiguration()).thenReturn(dynamicPushLatencyConfiguration);
when(dynamicPushLatencyConfiguration.instrumentedVersions()).thenReturn(Collections.emptyMap());
}
@ParameterizedTest
@MethodSource
void testTakeRecord(final boolean isVoip, final boolean isUrgent) throws ExecutionException, InterruptedException {
@ -59,7 +40,7 @@ class PushLatencyManagerTest {
final Instant pushTimestamp = Instant.now();
final PushLatencyManager pushLatencyManager = new PushLatencyManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(),
dynamicConfigurationManager, Clock.fixed(pushTimestamp, ZoneId.systemDefault()));
mock(ClientReleaseManager.class), Clock.fixed(pushTimestamp, ZoneId.systemDefault()));
assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get());

View File

@ -25,7 +25,6 @@ import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -45,15 +44,13 @@ import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicDeliveryLatencyConfiguration;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
@ -83,7 +80,7 @@ class WebSocketConnectionIntegrationTest {
private Device device;
private WebSocketClient webSocketClient;
private Scheduler messageDeliveryScheduler;
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private ClientReleaseManager clientReleaseManager;
private long serialTimestamp = System.currentTimeMillis();
@ -102,15 +99,7 @@ class WebSocketConnectionIntegrationTest {
account = mock(Account.class);
device = mock(Device.class);
webSocketClient = mock(WebSocketClient.class);
final DynamicDeliveryLatencyConfiguration deliveryLatencyConfiguration = mock(DynamicDeliveryLatencyConfiguration.class);
when(deliveryLatencyConfiguration.instrumentedVersions()).thenReturn(Collections.emptyMap());
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfiguration.getDeliveryLatencyConfiguration()).thenReturn(deliveryLatencyConfiguration);
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
clientReleaseManager = mock(ClientReleaseManager.class);
when(account.getNumber()).thenReturn("+18005551234");
when(account.getUuid()).thenReturn(UUID.randomUUID());
@ -140,7 +129,8 @@ class WebSocketConnectionIntegrationTest {
device,
webSocketClient,
scheduledExecutorService,
messageDeliveryScheduler, dynamicConfigurationManager);
messageDeliveryScheduler,
clientReleaseManager);
final List<MessageProtos.Envelope> expectedMessages = new ArrayList<>(persistedMessageCount + cachedMessageCount);
@ -224,7 +214,8 @@ class WebSocketConnectionIntegrationTest {
device,
webSocketClient,
scheduledExecutorService,
messageDeliveryScheduler, dynamicConfigurationManager);
messageDeliveryScheduler,
clientReleaseManager);
final int persistedMessageCount = 207;
final int cachedMessageCount = 173;
@ -291,7 +282,7 @@ class WebSocketConnectionIntegrationTest {
100, // use a very short timeout, so that this test completes quickly
scheduledExecutorService,
messageDeliveryScheduler,
dynamicConfigurationManager);
clientReleaseManager);
final int persistedMessageCount = 207;
final int cachedMessageCount = 173;

View File

@ -33,7 +33,6 @@ import io.lettuce.core.RedisException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -53,16 +52,14 @@ import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicDeliveryLatencyConfiguration;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.websocket.WebSocketClient;
@ -93,7 +90,7 @@ class WebSocketConnectionTest {
private ReceiptSender receiptSender;
private ScheduledExecutorService retrySchedulingExecutor;
private Scheduler messageDeliveryScheduler;
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private ClientReleaseManager clientReleaseManager;
@BeforeEach
void setup() {
@ -107,15 +104,7 @@ class WebSocketConnectionTest {
receiptSender = mock(ReceiptSender.class);
retrySchedulingExecutor = mock(ScheduledExecutorService.class);
messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery");
final DynamicDeliveryLatencyConfiguration deliveryLatencyConfiguration = mock(DynamicDeliveryLatencyConfiguration.class);
when(deliveryLatencyConfiguration.instrumentedVersions()).thenReturn(Collections.emptyMap());
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfiguration.getDeliveryLatencyConfiguration()).thenReturn(deliveryLatencyConfiguration);
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
clientReleaseManager = mock(ClientReleaseManager.class);
}
@AfterEach
@ -129,7 +118,7 @@ class WebSocketConnectionTest {
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(receiptSender, messagesManager,
mock(PushNotificationManager.class), mock(ClientPresenceManager.class),
retrySchedulingExecutor, messageDeliveryScheduler, dynamicConfigurationManager);
retrySchedulingExecutor, messageDeliveryScheduler, clientReleaseManager);
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
@ -209,7 +198,7 @@ class WebSocketConnectionTest {
});
WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager,
auth, device, client, retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
auth, device, client, retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
connection.start();
verify(client, times(3)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class),
@ -237,7 +226,7 @@ class WebSocketConnectionTest {
public void testOnlineSend() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
final UUID accountUuid = UUID.randomUUID();
@ -357,7 +346,7 @@ class WebSocketConnectionTest {
});
WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager,
auth, device, client, retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
auth, device, client, retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
connection.start();
@ -381,7 +370,7 @@ class WebSocketConnectionTest {
void testProcessStoredMessageConcurrency() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
when(account.getNumber()).thenReturn("+18005551234");
when(account.getUuid()).thenReturn(UUID.randomUUID());
@ -446,7 +435,7 @@ class WebSocketConnectionTest {
void testProcessStoredMessagesMultiplePages() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
when(account.getNumber()).thenReturn("+18005551234");
final UUID accountUuid = UUID.randomUUID();
@ -498,7 +487,7 @@ class WebSocketConnectionTest {
void testProcessStoredMessagesContainsSenderUuid() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
when(account.getNumber()).thenReturn("+18005551234");
final UUID accountUuid = UUID.randomUUID();
@ -560,7 +549,7 @@ class WebSocketConnectionTest {
void testProcessStoredMessagesSingleEmptyCall() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
final UUID accountUuid = UUID.randomUUID();
@ -589,7 +578,7 @@ class WebSocketConnectionTest {
public void testRequeryOnStateMismatch() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
final UUID accountUuid = UUID.randomUUID();
when(account.getNumber()).thenReturn("+18005551234");
@ -645,7 +634,7 @@ class WebSocketConnectionTest {
void testProcessCachedMessagesOnly() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
final UUID accountUuid = UUID.randomUUID();
@ -677,7 +666,7 @@ class WebSocketConnectionTest {
void testProcessDatabaseMessagesAfterPersist() {
final WebSocketClient client = mock(WebSocketClient.class);
final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
final UUID accountUuid = UUID.randomUUID();
@ -724,7 +713,7 @@ class WebSocketConnectionTest {
when(client.isOpen()).thenReturn(true);
WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
connection.start();
verify(retrySchedulingExecutor, times(WebSocketConnection.MAX_CONSECUTIVE_RETRIES)).schedule(any(Runnable.class),
@ -748,7 +737,7 @@ class WebSocketConnectionTest {
when(client.isOpen()).thenReturn(false);
WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
connection.start();
verify(retrySchedulingExecutor, never()).schedule(any(Runnable.class), anyLong(), any());
@ -782,7 +771,7 @@ class WebSocketConnectionTest {
CompletableFuture.completedFuture(Optional.empty()));
WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, messageDeliveryScheduler, dynamicConfigurationManager);
retrySchedulingExecutor, messageDeliveryScheduler, clientReleaseManager);
connection.start();
@ -839,7 +828,7 @@ class WebSocketConnectionTest {
CompletableFuture.completedFuture(Optional.empty()));
WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client,
retrySchedulingExecutor, Schedulers.immediate(), dynamicConfigurationManager);
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
connection.start();