Add UX for handling CDS rate limits.

This commit is contained in:
Greyson Parrelli
2022-11-10 10:51:21 -05:00
parent 8eb3a1906e
commit c563ef27da
21 changed files with 570 additions and 18 deletions

View File

@@ -0,0 +1,16 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* Indicates that something about our request was wrong. Could be:
* - Over 50k new contacts
* - Missing version byte prefix
* - Missing credentials
* - E164s are not a multiple of 8 bytes
* - Something else?
*/
public class CdsiInvalidArgumentException extends NonSuccessfulResponseCodeException {
public CdsiInvalidArgumentException() {
super(4003);
}
}

View File

@@ -0,0 +1,11 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* Indicates that you provided a bad token to CDSI.
*/
public class CdsiInvalidTokenException extends NonSuccessfulResponseCodeException {
public CdsiInvalidTokenException() {
super(4101);
}
}

View File

@@ -0,0 +1,18 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* A 4008 responses from CDSI indicating we've exhausted our quota.
*/
public class CdsiResourceExhaustedException extends NonSuccessfulResponseCodeException {
private final int retryAfterSeconds;
public CdsiResourceExhaustedException(int retryAfterSeconds) {
super(4008);
this.retryAfterSeconds = retryAfterSeconds;
}
public int getRetryAfterSeconds() {
return retryAfterSeconds;
}
}

View File

@@ -8,7 +8,10 @@ import org.signal.libsignal.cds2.Cds2CommunicationFailureException;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
import org.whispersystems.signalservice.api.util.TlsProxySocketFactory;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@@ -22,9 +25,9 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@@ -90,6 +93,9 @@ final class CdsiSocket {
.addHeader("Authorization", basicAuth(username, password))
.build();
AtomicInteger retryAfterSeconds = new AtomicInteger(0);
WebSocket webSocket = okhttp.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
@@ -128,6 +134,12 @@ final class CdsiSocket {
case WAITING_FOR_TOKEN:
ClientResponse tokenResponse = ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray()));
if (tokenResponse.getRetryAfterSecs() > 0) {
Log.w(TAG, "Got a retry-after (" + tokenResponse.getRetryAfterSecs() + "), meaning we hit the rate limit. (WAITING_FOR_TOKEN)");
throw new CdsiResourceExhaustedException(tokenResponse.getRetryAfterSecs());
}
if (tokenResponse.getToken().isEmpty()) {
throw new IOException("No token! Cannot continue!");
}
@@ -143,6 +155,13 @@ final class CdsiSocket {
break;
case WAITING_FOR_RESPONSE:
ClientResponse dataResponse = ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray()));
if (dataResponse.getRetryAfterSecs() > 0) {
Log.w(TAG, "Got a retry-after (" + dataResponse.getRetryAfterSecs() + "), meaning we hit the rate limit. (WAITING_FOR_RESPONSE)");
throw new CdsiResourceExhaustedException(dataResponse.getRetryAfterSecs());
}
emitter.onNext(ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray())));
break;
@@ -172,7 +191,13 @@ final class CdsiSocket {
Log.w(TAG, "Remote side is closing with non-normal code " + code);
webSocket.close(1000, "Remote closed with code " + code);
stage.set(Stage.FAILED);
emitter.tryOnError(new NonSuccessfulResponseCodeException(code));
if (code == 4003) {
emitter.tryOnError(new CdsiInvalidArgumentException());
} else if (code == 4101) {
emitter.tryOnError(new CdsiInvalidTokenException());
} else {
emitter.tryOnError(new NonSuccessfulResponseCodeException(code));
}
}
}

View File

@@ -40,14 +40,14 @@ public final class CdsiV2Service {
private static final UUID EMPTY_UUID = new UUID(0, 0);
private static final int RESPONSE_ITEM_SIZE = 8 + 16 + 16; // 1 uint64 + 2 UUIDs
private final CdsiSocket cdshSocket;
private final CdsiSocket cdsiSocket;
public CdsiV2Service(SignalServiceConfiguration configuration, String mrEnclave) {
this.cdshSocket = new CdsiSocket(configuration, mrEnclave);
this.cdsiSocket = new CdsiSocket(configuration, mrEnclave);
}
public Single<ServiceResponse<Response>> getRegisteredUsers(String username, String password, Request request, Consumer<byte[]> tokenSaver) {
return cdshSocket
return cdsiSocket
.connect(username, password, buildClientRequest(request), tokenSaver)
.map(CdsiV2Service::parseEntries)
.collect(Collectors.toList())