Create a new system for fetching the intial batch of messages.

This commit is contained in:
Greyson Parrelli
2020-05-20 09:06:08 -04:00
parent d2bf539504
commit a299bafe89
17 changed files with 522 additions and 117 deletions

View File

@@ -12,6 +12,8 @@ import com.google.firebase.messaging.RemoteMessage;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.RestStrategy;
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@@ -68,8 +70,8 @@ public class FcmFetchService extends Service {
}
private void fetch() {
MessageRetriever retriever = ApplicationDependencies.getMessageRetriever();
boolean success = retriever.retrieveMessages(this, new RestStrategy(), new RestStrategy());
BackgroundMessageRetriever retriever = ApplicationDependencies.getBackgroundMessageRetriever();
boolean success = retriever.retrieveMessages(this, new RestStrategy(), new RestStrategy());
if (success) {
Log.i(TAG, "Successfully retrieved messages.");

View File

@@ -11,6 +11,8 @@ import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.RestStrategy;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@@ -39,15 +41,15 @@ public class FcmJobService extends JobService {
public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob()");
if (MessageRetriever.shouldIgnoreFetch(this)) {
if (BackgroundMessageRetriever.shouldIgnoreFetch(this)) {
Log.i(TAG, "App is foregrounded. No need to run.");
return false;
}
SignalExecutors.UNBOUNDED.execute(() -> {
Context context = getApplicationContext();
MessageRetriever retriever = ApplicationDependencies.getMessageRetriever();
boolean success = retriever.retrieveMessages(context, new RestStrategy(), new RestStrategy());
Context context = getApplicationContext();
BackgroundMessageRetriever retriever = ApplicationDependencies.getBackgroundMessageRetriever();
boolean success = retriever.retrieveMessages(context, new RestStrategy(), new RestStrategy());
if (success) {
Log.i(TAG, "Successfully retrieved messages.");

View File

@@ -1,121 +0,0 @@
package org.thoughtcrime.securesms.gcm;
import android.content.Context;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.PowerManagerCompat;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.WakeLockUtil;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Facilitates the retrieval of messages via provided {@link Strategy}'s.
*/
public class MessageRetriever {
private static final String TAG = Log.tag(MessageRetriever.class);
private static final String WAKE_LOCK_TAG = "MessageRetriever";
private static final Semaphore ACTIVE_LOCK = new Semaphore(2);
/**
* @return False if the retrieval failed and should be rescheduled, otherwise true.
*/
@WorkerThread
public boolean retrieveMessages(@NonNull Context context, Strategy... strategies) {
if (shouldIgnoreFetch(context)) {
Log.i(TAG, "Skipping retrieval -- app is in the foreground.");
return true;
}
if (!ACTIVE_LOCK.tryAcquire()) {
Log.i(TAG, "Skipping retrieval -- there's already one enqueued.");
return true;
}
synchronized (this) {
PowerManager.WakeLock wakeLock = null;
try {
wakeLock = WakeLockUtil.acquire(context, PowerManager.PARTIAL_WAKE_LOCK, TimeUnit.SECONDS.toMillis(60), WAKE_LOCK_TAG);
TextSecurePreferences.setNeedsMessagePull(context, true);
long startTime = System.currentTimeMillis();
PowerManager powerManager = ServiceUtil.getPowerManager(context);
boolean doze = PowerManagerCompat.isDeviceIdleMode(powerManager);
boolean network = new NetworkConstraint.Factory(ApplicationContext.getInstance(context)).create().isMet();
if (doze || !network) {
Log.w(TAG, "We may be operating in a constrained environment. Doze: " + doze + " Network: " + network);
}
boolean success = false;
for (Strategy strategy : strategies) {
if (shouldIgnoreFetch(context)) {
Log.i(TAG, "Stopping further strategy attempts -- app is in the foreground." + logSuffix(startTime));
success = true;
break;
}
Log.i(TAG, "Attempting strategy: " + strategy.toString() + logSuffix(startTime));
if (strategy.run()) {
Log.i(TAG, "Strategy succeeded: " + strategy.toString() + logSuffix(startTime));
success = true;
break;
} else {
Log.w(TAG, "Strategy failed: " + strategy.toString() + logSuffix(startTime));
}
}
if (success) {
TextSecurePreferences.setNeedsMessagePull(context, false);
} else {
Log.w(TAG, "All strategies failed!" + logSuffix(startTime));
}
return success;
} finally {
WakeLockUtil.release(wakeLock, WAKE_LOCK_TAG);
ACTIVE_LOCK.release();
}
}
}
/**
* @return True if there is no need to execute a message fetch, because the websocket will take
* care of it.
*/
public static boolean shouldIgnoreFetch(@NonNull Context context) {
return ApplicationContext.getInstance(context).isAppVisible() &&
!ApplicationDependencies.getSignalServiceNetworkAccess().isCensored(context);
}
private static String logSuffix(long startTime) {
return " (" + (System.currentTimeMillis() - startTime) + " ms elapsed)";
}
/**
* A method of retrieving messages.
*/
public interface Strategy {
/**
* @return False if the message retrieval failed and should be retried, otherwise true.
*/
@WorkerThread
boolean run();
}
}

View File

@@ -1,151 +0,0 @@
package org.thoughtcrime.securesms.gcm;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.IncomingMessageProcessor;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JobTracker;
import org.thoughtcrime.securesms.jobs.MarkerJob;
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Retrieves messages over the REST endpoint.
*/
public class RestStrategy implements MessageRetriever.Strategy {
private static final String TAG = Log.tag(RestStrategy.class);
private static final long SOCKET_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
@WorkerThread
@Override
public boolean run() {
long startTime = System.currentTimeMillis();
JobManager jobManager = ApplicationDependencies.getJobManager();
QueueFindingJobListener queueListener = new QueueFindingJobListener();
try (IncomingMessageProcessor.Processor processor = ApplicationDependencies.getIncomingMessageProcessor().acquire()) {
int jobCount = enqueuePushDecryptJobs(processor, startTime);
if (jobCount == 0) {
Log.d(TAG, "No PushDecryptMessageJobs were enqueued.");
return true;
} else {
Log.d(TAG, jobCount + " PushDecryptMessageJob(s) were enqueued.");
}
jobManager.addListener(job -> job.getParameters().getQueue() != null && job.getParameters().getQueue().startsWith(PushProcessMessageJob.QUEUE_PREFIX), queueListener);
long timeRemainingMs = blockUntilQueueDrained(PushDecryptMessageJob.QUEUE, TimeUnit.SECONDS.toMillis(10));
Set<String> processQueues = queueListener.getQueues();
Log.d(TAG, "Discovered " + processQueues.size() + " queue(s): " + processQueues);
if (timeRemainingMs > 0) {
Iterator<String> iter = processQueues.iterator();
while (iter.hasNext() && timeRemainingMs > 0) {
timeRemainingMs = blockUntilQueueDrained(iter.next(), timeRemainingMs);
}
if (timeRemainingMs <= 0) {
Log.w(TAG, "Ran out of time while waiting for queues to drain.");
}
} else {
Log.w(TAG, "Ran out of time before we could even wait on individual queues!");
}
return true;
} catch (IOException e) {
Log.w(TAG, "Failed to retrieve messages. Resetting the SignalServiceMessageReceiver.", e);
ApplicationDependencies.resetSignalServiceMessageReceiver();
return false;
} finally {
jobManager.removeListener(queueListener);
}
}
private static int enqueuePushDecryptJobs(IncomingMessageProcessor.Processor processor, long startTime)
throws IOException
{
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
AtomicInteger jobCount = new AtomicInteger(0);
receiver.setSoTimeoutMillis(SOCKET_TIMEOUT);
receiver.retrieveMessages(envelope -> {
Log.i(TAG, "Retrieved an envelope." + timeSuffix(startTime));
String jobId = processor.processEnvelope(envelope);
if (jobId != null) {
jobCount.incrementAndGet();
}
Log.i(TAG, "Successfully processed an envelope." + timeSuffix(startTime));
});
return jobCount.get();
}
private static long blockUntilQueueDrained(@NonNull String queue, long timeoutMs) {
long startTime = System.currentTimeMillis();
final JobManager jobManager = ApplicationDependencies.getJobManager();
final MarkerJob markerJob = new MarkerJob(queue);
Optional<JobTracker.JobState> jobState = jobManager.runSynchronously(markerJob, timeoutMs);
if (!jobState.isPresent()) {
Log.w(TAG, "Timed out waiting for " + queue + " job(s) to finish!");
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
Log.d(TAG, "Waited " + duration + " ms for the " + queue + " job(s) to finish.");
return timeoutMs - duration;
}
private static String timeSuffix(long startTime) {
return " (" + (System.currentTimeMillis() - startTime) + " ms elapsed)";
}
@Override
public @NonNull String toString() {
return RestStrategy.class.getSimpleName();
}
private static class QueueFindingJobListener implements JobTracker.JobListener {
private final Set<String> queues = new HashSet<>();
@Override
@AnyThread
public void onStateChanged(@NonNull Job job, @NonNull JobTracker.JobState jobState) {
synchronized (queues) {
queues.add(job.getParameters().getQueue());
}
}
@NonNull Set<String> getQueues() {
synchronized (queues) {
return new HashSet<>(queues);
}
}
}
}