mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Remove libsignal shadow/bridge websocket infra.
This commit is contained in:
committed by
Michelle Tang
parent
83611414cc
commit
da5c8ff6ea
@@ -47,7 +47,6 @@ import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
|||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||||
import org.thoughtcrime.securesms.net.DefaultWebSocketShadowingBridge;
|
|
||||||
import org.thoughtcrime.securesms.net.SignalWebSocketHealthMonitor;
|
import org.thoughtcrime.securesms.net.SignalWebSocketHealthMonitor;
|
||||||
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
|
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||||
@@ -105,9 +104,7 @@ import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
|||||||
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.LibSignalNetworkExtensions;
|
import org.whispersystems.signalservice.internal.websocket.LibSignalNetworkExtensions;
|
||||||
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.ShadowingWebSocketConnection;
|
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketShadowingBridge;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -302,8 +299,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||||||
public @NonNull SignalWebSocket.AuthenticatedWebSocket provideAuthWebSocket(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier, @NonNull Supplier<Network> libSignalNetworkSupplier) {
|
public @NonNull SignalWebSocket.AuthenticatedWebSocket provideAuthWebSocket(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier, @NonNull Supplier<Network> libSignalNetworkSupplier) {
|
||||||
SleepTimer sleepTimer = !SignalStore.account().isFcmEnabled() || SignalStore.internal().isWebsocketModeForced() ? new AlarmSleepTimer(context) : new UptimeSleepTimer();
|
SleepTimer sleepTimer = !SignalStore.account().isFcmEnabled() || SignalStore.internal().isWebsocketModeForced() ? new AlarmSleepTimer(context) : new UptimeSleepTimer();
|
||||||
SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(sleepTimer);
|
SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(sleepTimer);
|
||||||
WebSocketShadowingBridge bridge = new DefaultWebSocketShadowingBridge(context);
|
WebSocketFactory webSocketFactory = provideWebSocketFactory(signalServiceConfigurationSupplier, healthMonitor, libSignalNetworkSupplier);
|
||||||
WebSocketFactory webSocketFactory = provideWebSocketFactory(signalServiceConfigurationSupplier, healthMonitor, libSignalNetworkSupplier, bridge);
|
|
||||||
SignalWebSocket.AuthenticatedWebSocket webSocket = new SignalWebSocket.AuthenticatedWebSocket(webSocketFactory::createWebSocket);
|
SignalWebSocket.AuthenticatedWebSocket webSocket = new SignalWebSocket.AuthenticatedWebSocket(webSocketFactory::createWebSocket);
|
||||||
|
|
||||||
healthMonitor.monitor(webSocket);
|
healthMonitor.monitor(webSocket);
|
||||||
@@ -315,8 +311,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||||||
public @NonNull SignalWebSocket.UnauthenticatedWebSocket provideUnauthWebSocket(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier, @NonNull Supplier<Network> libSignalNetworkSupplier) {
|
public @NonNull SignalWebSocket.UnauthenticatedWebSocket provideUnauthWebSocket(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier, @NonNull Supplier<Network> libSignalNetworkSupplier) {
|
||||||
SleepTimer sleepTimer = !SignalStore.account().isFcmEnabled() || SignalStore.internal().isWebsocketModeForced() ? new AlarmSleepTimer(context) : new UptimeSleepTimer();
|
SleepTimer sleepTimer = !SignalStore.account().isFcmEnabled() || SignalStore.internal().isWebsocketModeForced() ? new AlarmSleepTimer(context) : new UptimeSleepTimer();
|
||||||
SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(sleepTimer);
|
SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(sleepTimer);
|
||||||
WebSocketShadowingBridge bridge = new DefaultWebSocketShadowingBridge(context);
|
WebSocketFactory webSocketFactory = provideWebSocketFactory(signalServiceConfigurationSupplier, healthMonitor, libSignalNetworkSupplier);
|
||||||
WebSocketFactory webSocketFactory = provideWebSocketFactory(signalServiceConfigurationSupplier, healthMonitor, libSignalNetworkSupplier, bridge);
|
|
||||||
SignalWebSocket.UnauthenticatedWebSocket webSocket = new SignalWebSocket.UnauthenticatedWebSocket(webSocketFactory::createUnidentifiedWebSocket);
|
SignalWebSocket.UnauthenticatedWebSocket webSocket = new SignalWebSocket.UnauthenticatedWebSocket(webSocketFactory::createUnidentifiedWebSocket);
|
||||||
|
|
||||||
healthMonitor.monitor(webSocket);
|
healthMonitor.monitor(webSocket);
|
||||||
@@ -419,21 +414,18 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||||||
|
|
||||||
@NonNull WebSocketFactory provideWebSocketFactory(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier,
|
@NonNull WebSocketFactory provideWebSocketFactory(@NonNull Supplier<SignalServiceConfiguration> signalServiceConfigurationSupplier,
|
||||||
@NonNull HealthMonitor healthMonitor,
|
@NonNull HealthMonitor healthMonitor,
|
||||||
@NonNull Supplier<Network> libSignalNetworkSupplier,
|
@NonNull Supplier<Network> libSignalNetworkSupplier)
|
||||||
@NonNull WebSocketShadowingBridge bridge)
|
|
||||||
{
|
{
|
||||||
return new WebSocketFactory() {
|
return new WebSocketFactory() {
|
||||||
@Override
|
@Override
|
||||||
public WebSocketConnection createWebSocket() {
|
public WebSocketConnection createWebSocket() {
|
||||||
if (RemoteConfig.libSignalWebSocketEnabled()) {
|
if (RemoteConfig.libSignalWebSocketEnabled()) {
|
||||||
Network network = libSignalNetworkSupplier.get();
|
Network network = libSignalNetworkSupplier.get();
|
||||||
return new LibSignalChatConnection(
|
return new LibSignalChatConnection("libsignal-auth",
|
||||||
"libsignal-auth",
|
network,
|
||||||
network,
|
new DynamicCredentialsProvider(),
|
||||||
new DynamicCredentialsProvider(),
|
Stories.isFeatureEnabled(),
|
||||||
Stories.isFeatureEnabled(),
|
healthMonitor);
|
||||||
healthMonitor
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return new OkHttpWebSocketConnection("normal",
|
return new OkHttpWebSocketConnection("normal",
|
||||||
signalServiceConfigurationSupplier.get(),
|
signalServiceConfigurationSupplier.get(),
|
||||||
@@ -446,28 +438,13 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WebSocketConnection createUnidentifiedWebSocket() {
|
public WebSocketConnection createUnidentifiedWebSocket() {
|
||||||
int shadowPercentage = RemoteConfig.libSignalWebSocketShadowingPercentage();
|
|
||||||
if (shadowPercentage > 0) {
|
|
||||||
return new ShadowingWebSocketConnection(
|
|
||||||
"unauth-shadow",
|
|
||||||
signalServiceConfigurationSupplier.get(),
|
|
||||||
Optional.empty(),
|
|
||||||
BuildConfig.SIGNAL_AGENT,
|
|
||||||
healthMonitor,
|
|
||||||
Stories.isFeatureEnabled(),
|
|
||||||
libSignalNetworkSupplier.get(),
|
|
||||||
shadowPercentage,
|
|
||||||
bridge
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (RemoteConfig.libSignalWebSocketEnabled()) {
|
if (RemoteConfig.libSignalWebSocketEnabled()) {
|
||||||
Network network = libSignalNetworkSupplier.get();
|
Network network = libSignalNetworkSupplier.get();
|
||||||
return new LibSignalChatConnection(
|
return new LibSignalChatConnection("libsignal-unauth",
|
||||||
"libsignal-unauth",
|
network,
|
||||||
network,
|
null,
|
||||||
null,
|
Stories.isFeatureEnabled(),
|
||||||
Stories.isFeatureEnabled(),
|
healthMonitor);
|
||||||
healthMonitor);
|
|
||||||
} else {
|
} else {
|
||||||
return new OkHttpWebSocketConnection("unidentified",
|
return new OkHttpWebSocketConnection("unidentified",
|
||||||
signalServiceConfigurationSupplier.get(),
|
signalServiceConfigurationSupplier.get(),
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.net
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import org.signal.core.util.PendingIntentFlags
|
|
||||||
import org.thoughtcrime.securesms.R
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.InternalValues
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
|
||||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketShadowingBridge
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements a [WebSocketShadowingBridge] to provide shadowing-specific functionality to
|
|
||||||
* [org.whispersystems.signalservice.internal.websocket.ShadowingWebSocketConnection]
|
|
||||||
*/
|
|
||||||
class DefaultWebSocketShadowingBridge(private val context: Application) : WebSocketShadowingBridge {
|
|
||||||
private val store: InternalValues = SignalStore.internal
|
|
||||||
|
|
||||||
override fun writeStatsSnapshot(bytes: ByteArray) {
|
|
||||||
store.webSocketShadowingStats = bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun readStatsSnapshot(): ByteArray? {
|
|
||||||
return store.webSocketShadowingStats
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun triggerFailureNotification(message: String) {
|
|
||||||
if (!RemoteConfig.internalUser) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setContentTitle("[Internal-only] $message")
|
|
||||||
.setContentText("Tap to send a debug log")
|
|
||||||
.setContentIntent(
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
Intent(context, SubmitDebugLogActivity::class.java),
|
|
||||||
PendingIntentFlags.mutable()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1036,20 +1036,6 @@ object RemoteConfig {
|
|||||||
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false)
|
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Percentage [0, 100] of web socket requests that will be "shadowed" by sending
|
|
||||||
* an unauthenticated keep-alive via libsignal-net. Default: 0
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
@get:JvmName("libSignalWebSocketShadowingPercentage")
|
|
||||||
val libSignalWebSocketShadowingPercentage: Int by remoteValue(
|
|
||||||
key = "android.libsignalWebSocketShadowingPercentage",
|
|
||||||
hotSwappable = false
|
|
||||||
) { value ->
|
|
||||||
val remote = value.asInteger(0)
|
|
||||||
remote.coerceIn(0, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val backgroundMessageProcessInterval: Long by remoteValue(
|
val backgroundMessageProcessInterval: Long by remoteValue(
|
||||||
key = "android.messageProcessor.alarmIntervalMins",
|
key = "android.messageProcessor.alarmIntervalMins",
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.signalservice.internal.websocket
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.WebSocket
|
|
||||||
import org.signal.core.util.logging.Log
|
|
||||||
import org.signal.libsignal.net.ChatConnection
|
|
||||||
import org.signal.libsignal.net.Network
|
|
||||||
import org.signal.libsignal.net.UnauthenticatedChatConnection
|
|
||||||
import org.whispersystems.signalservice.api.util.CredentialsProvider
|
|
||||||
import org.whispersystems.signalservice.api.websocket.HealthMonitor
|
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
|
||||||
import org.whispersystems.signalservice.internal.util.whenComplete
|
|
||||||
import java.util.Optional
|
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper on top of [OkHttpWebSocketConnection] that sends a keep alive via
|
|
||||||
* libsignal-net for a configurable percentage of the _successful_ web socket requests.
|
|
||||||
*
|
|
||||||
* Stats are collected for the shadowed requests and persisted across app restarts
|
|
||||||
* using [org.thoughtcrime.securesms.keyvalue.InternalValues].
|
|
||||||
*
|
|
||||||
* When a hardcoded error threshold is reached, the user is notified to submit debug logs.
|
|
||||||
*
|
|
||||||
* @see [org.thoughtcrime.securesms.util.RemoteConfig.libSignalWebSocketShadowingPercentage]
|
|
||||||
*/
|
|
||||||
class ShadowingWebSocketConnection(
|
|
||||||
name: String,
|
|
||||||
serviceConfiguration: SignalServiceConfiguration,
|
|
||||||
credentialsProvider: Optional<CredentialsProvider>,
|
|
||||||
signalAgent: String,
|
|
||||||
healthMonitor: HealthMonitor,
|
|
||||||
allowStories: Boolean,
|
|
||||||
private val network: Network,
|
|
||||||
private val shadowPercentage: Int,
|
|
||||||
private val bridge: WebSocketShadowingBridge
|
|
||||||
) : OkHttpWebSocketConnection(
|
|
||||||
name,
|
|
||||||
serviceConfiguration,
|
|
||||||
credentialsProvider,
|
|
||||||
signalAgent,
|
|
||||||
healthMonitor,
|
|
||||||
allowStories
|
|
||||||
) {
|
|
||||||
private var stats: Stats = try {
|
|
||||||
bridge.readStatsSnapshot()?.let {
|
|
||||||
Stats.fromSnapshot(it)
|
|
||||||
} ?: Stats()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Log.w(TAG, "Failed to restore Stats from a snapshot.", ex)
|
|
||||||
Stats()
|
|
||||||
}
|
|
||||||
private val canShadow: AtomicBoolean = AtomicBoolean(false)
|
|
||||||
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
||||||
private var chatConnection: UnauthenticatedChatConnection? = null
|
|
||||||
private var shadowingConnectPending = false
|
|
||||||
|
|
||||||
override fun connect(): Observable<WebSocketConnectionState> {
|
|
||||||
// NB: The potential for race conditions here was introduced when we switched from ChatService's
|
|
||||||
// long lived connection model to the single-use ChatConnection model.
|
|
||||||
// At this time, we do not intend to ever use this code in production again, so I'm deferring properly
|
|
||||||
// fixing it with a refactor, and instead just doing the bare minimum to avoid an obvious race.
|
|
||||||
// If we do want to use this again in production, we should probably refactor to depend on the higher level
|
|
||||||
// LibSignalChatConnection, rather than the lower level ChatConnection API.
|
|
||||||
if (chatConnection == null && !shadowingConnectPending) {
|
|
||||||
shadowingConnectPending = true
|
|
||||||
executor.submit {
|
|
||||||
network.connectUnauthChat(null).whenComplete(
|
|
||||||
onSuccess = { connection ->
|
|
||||||
shadowingConnectPending = false
|
|
||||||
chatConnection = connection
|
|
||||||
canShadow.set(true)
|
|
||||||
Log.i(TAG, "Shadow socket connected.")
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
shadowingConnectPending = false
|
|
||||||
canShadow.set(false)
|
|
||||||
Log.i(TAG, "Shadow socket failed to connect.")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
|
||||||
saveSnapshot()
|
|
||||||
super.onClosing(webSocket, code, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
|
||||||
saveSnapshot()
|
|
||||||
super.onFailure(webSocket, t, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disconnect() {
|
|
||||||
executor.submit {
|
|
||||||
chatConnection?.disconnect()?.thenApply {
|
|
||||||
canShadow.set(false)
|
|
||||||
Log.i(TAG, "Shadow socket disconnected.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendRequest(request: WebSocketRequestMessage): Single<WebsocketResponse> {
|
|
||||||
return super.sendRequest(request).doOnSuccess(::sendShadow)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendShadow(actualResponse: WebsocketResponse) {
|
|
||||||
executor.submit {
|
|
||||||
if (canShadow.get() && Random.nextInt(100) < this.shadowPercentage) {
|
|
||||||
libsignalKeepAlive(actualResponse)
|
|
||||||
val snapshotAge = System.currentTimeMillis() - stats.lastSnapshot.get()
|
|
||||||
if (snapshotAge > SNAPSHOT_INTERVAL.inWholeMilliseconds) {
|
|
||||||
saveSnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldSubmitLogs(): Boolean {
|
|
||||||
val requestsCompared = stats.requestsCompared.get()
|
|
||||||
// Should not happen in practice, but helps avoid a division by zero later if it does.
|
|
||||||
if (requestsCompared == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val timeSinceLastNotificationMs = System.currentTimeMillis() - stats.lastNotified.get()
|
|
||||||
val percentFailed = stats.failures.get() * 100 / requestsCompared
|
|
||||||
return timeSinceLastNotificationMs > FULL_DAY.inWholeMilliseconds &&
|
|
||||||
percentFailed > FAILURE_PERCENTAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun libsignalKeepAlive(actualResponse: WebsocketResponse) {
|
|
||||||
val connection = chatConnection ?: return
|
|
||||||
val request = ChatConnection.Request(
|
|
||||||
"GET",
|
|
||||||
"/v1/keepalive",
|
|
||||||
emptyMap(),
|
|
||||||
ByteArray(0),
|
|
||||||
KEEP_ALIVE_TIMEOUT.inWholeMilliseconds.toInt()
|
|
||||||
)
|
|
||||||
connection.send(request)
|
|
||||||
?.whenComplete(
|
|
||||||
onSuccess = { response ->
|
|
||||||
stats.requestsCompared.incrementAndGet()
|
|
||||||
val goodStatus = (response?.status ?: -1) in 200..299
|
|
||||||
if (!goodStatus) {
|
|
||||||
stats.badStatuses.incrementAndGet()
|
|
||||||
}
|
|
||||||
Log.i(TAG, response?.message)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
stats.requestsCompared.incrementAndGet()
|
|
||||||
stats.failures.incrementAndGet()
|
|
||||||
Log.w(TAG, "Shadow request failed: reason=[$it]")
|
|
||||||
Log.i(TAG, "Actual response status=${actualResponse.status}")
|
|
||||||
if (shouldSubmitLogs()) {
|
|
||||||
Log.i(TAG, "Notification to submit logs triggered.")
|
|
||||||
bridge.triggerFailureNotification("Experimental websocket transport failures!")
|
|
||||||
stats.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSnapshot() {
|
|
||||||
executor.submit {
|
|
||||||
Log.d(TAG, "Persisting shadowing stats snapshot.")
|
|
||||||
bridge.writeStatsSnapshot(stats.snapshot())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = Log.tag(ShadowingWebSocketConnection::class.java)
|
|
||||||
private val FULL_DAY: Duration = 1.days
|
|
||||||
|
|
||||||
// If more than this percentage of shadow requests fail, the
|
|
||||||
// notification to submit logs will be triggered.
|
|
||||||
private const val FAILURE_PERCENTAGE: Int = 10
|
|
||||||
private val KEEP_ALIVE_TIMEOUT: Duration = 3.seconds
|
|
||||||
private val SNAPSHOT_INTERVAL: Duration = 10.minutes
|
|
||||||
}
|
|
||||||
|
|
||||||
class Stats(
|
|
||||||
requestsCompared: Int = 0,
|
|
||||||
failures: Int = 0,
|
|
||||||
badStatuses: Int = 0,
|
|
||||||
lastNotified: Long = 0
|
|
||||||
) {
|
|
||||||
val requestsCompared: AtomicInteger = AtomicInteger(requestsCompared)
|
|
||||||
val failures: AtomicInteger = AtomicInteger(failures)
|
|
||||||
val badStatuses: AtomicInteger = AtomicInteger(badStatuses)
|
|
||||||
val lastNotified: AtomicLong = AtomicLong(lastNotified)
|
|
||||||
val lastSnapshot: AtomicLong = AtomicLong(0)
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
requestsCompared.set(0)
|
|
||||||
failures.set(0)
|
|
||||||
badStatuses.set(0)
|
|
||||||
// Do not reset lastNotified nor lastSnapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromSnapshot(bytes: ByteArray): Stats {
|
|
||||||
val snapshot = Snapshot.ADAPTER.decode(bytes)
|
|
||||||
return Stats(snapshot.requestsCompared, snapshot.failures, snapshot.badStatuses, snapshot.lastNotified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun snapshot(): ByteArray {
|
|
||||||
lastSnapshot.set(System.currentTimeMillis())
|
|
||||||
return Snapshot.Builder()
|
|
||||||
.requestsCompared(requestsCompared.get())
|
|
||||||
.failures(failures.get())
|
|
||||||
.badStatuses(badStatuses.get())
|
|
||||||
.lastNotified(lastNotified.get())
|
|
||||||
.build()
|
|
||||||
.encode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.signalservice.internal.websocket
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface to support app<->signal-service interop for the purposes of web socket shadowing.
|
|
||||||
*/
|
|
||||||
interface WebSocketShadowingBridge {
|
|
||||||
/**
|
|
||||||
* Persist shadowing stats snapshot.
|
|
||||||
*/
|
|
||||||
fun writeStatsSnapshot(bytes: ByteArray)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore shadowing stats snapshot.
|
|
||||||
*/
|
|
||||||
fun readStatsSnapshot(): ByteArray?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a notification the user to submit debug logs, with a custom message.
|
|
||||||
*/
|
|
||||||
fun triggerFailureNotification(message: String)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user