Allow RuntimeExceptions thrown by Jobs to crash.

This commit is contained in:
Greyson Parrelli 2019-07-25 19:55:26 -04:00 committed by Alan Evans
parent e8e80e5d05
commit 17400020b7
3 changed files with 91 additions and 23 deletions

View File

@ -125,8 +125,59 @@ public abstract class Job {
@NonNull T create(@NonNull Parameters parameters, @NonNull Data data); @NonNull T create(@NonNull Parameters parameters, @NonNull Data data);
} }
public enum Result { public static final class Result {
SUCCESS, FAILURE, RETRY
private static final Result SUCCESS = new Result(ResultType.SUCCESS, null);
private static final Result RETRY = new Result(ResultType.RETRY, null);
private static final Result FAILURE = new Result(ResultType.FAILURE, null);
private final ResultType resultType;
private final RuntimeException runtimeException;
private Result(@NonNull ResultType resultType, @Nullable RuntimeException runtimeException) {
this.resultType = resultType;
this.runtimeException = runtimeException;
}
/** Job completed successfully. */
public static Result success() {
return SUCCESS;
}
/** Job did not complete successfully, but it can be retried later. */
public static Result retry() {
return RETRY;
}
/** Job did not complete successfully and should not be tried again. Dependent jobs will also be failed.*/
public static Result failure() {
return FAILURE;
}
/** Same as {@link #failure()}, except the app should also crash with the provided exception. */
public static Result fatalFailure(@NonNull RuntimeException runtimeException) {
return new Result(ResultType.FAILURE, runtimeException);
}
boolean isSuccess() {
return resultType == ResultType.SUCCESS;
}
boolean isRetry() {
return resultType == ResultType.RETRY;
}
boolean isFailure() {
return resultType == ResultType.FAILURE;
}
@Nullable RuntimeException getException() {
return runtimeException;
}
private enum ResultType {
SUCCESS, FAILURE, RETRY
}
} }
public static final class Parameters { public static final class Parameters {

View File

@ -12,6 +12,14 @@ import org.thoughtcrime.securesms.util.WakeLockUtil;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/**
* A thread that constantly checks for available {@link Job}s owned by the {@link JobController}.
* When one is available, this class will execute it and call the appropriate methods on
* {@link JobController} based on the result.
*
* {@link JobRunner} and {@link JobController} were written such that you should be able to have
* N concurrent {@link JobRunner}s operating over the same {@link JobController}.
*/
class JobRunner extends Thread { class JobRunner extends Thread {
private static final String TAG = JobRunner.class.getSimpleName(); private static final String TAG = JobRunner.class.getSimpleName();
@ -32,25 +40,28 @@ class JobRunner extends Thread {
@Override @Override
public synchronized void run() { public synchronized void run() {
//noinspection InfiniteLoopStatement
while (true) { while (true) {
Job job = jobController.pullNextEligibleJobForExecution(); Job job = jobController.pullNextEligibleJobForExecution();
Job.Result result = run(job); Job.Result result = run(job);
jobController.onJobFinished(job); jobController.onJobFinished(job);
switch (result) { if (result.isSuccess()) {
case SUCCESS: jobController.onSuccess(job);
jobController.onSuccess(job); } else if (result.isRetry()) {
break; jobController.onRetry(job);
case RETRY: job.onRetry();
jobController.onRetry(job); } else if (result.isFailure()) {
job.onRetry(); List<Job> dependents = jobController.onFailure(job);
break; job.onCanceled();
case FAILURE: Stream.of(dependents).forEach(Job::onCanceled);
List<Job> dependents = jobController.onFailure(job);
job.onCanceled(); if (result.getException() != null) {
Stream.of(dependents).forEach(Job::onCanceled); throw result.getException();
break; }
} else {
throw new AssertionError("Invalid job result!");
} }
} }
} }
@ -60,7 +71,7 @@ class JobRunner extends Thread {
if (isJobExpired(job)) { if (isJobExpired(job)) {
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its lifespan.")); Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its lifespan."));
return Job.Result.FAILURE; return Job.Result.failure();
} }
Job.Result result = null; Job.Result result = null;
@ -71,7 +82,7 @@ class JobRunner extends Thread {
result = job.run(); result = job.run();
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing due to an unexpected exception."), e); Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing due to an unexpected exception."), e);
return Job.Result.FAILURE; return Job.Result.failure();
} finally { } finally {
if (wakeLock != null) { if (wakeLock != null) {
WakeLockUtil.release(wakeLock, job.getId()); WakeLockUtil.release(wakeLock, job.getId());
@ -80,11 +91,12 @@ class JobRunner extends Thread {
printResult(job, result); printResult(job, result);
if (result == Job.Result.RETRY && job.getRunAttempt() + 1 >= job.getParameters().getMaxAttempts() && if (result.isRetry() &&
job.getRunAttempt() + 1 >= job.getParameters().getMaxAttempts() &&
job.getParameters().getMaxAttempts() != Job.Parameters.UNLIMITED) job.getParameters().getMaxAttempts() != Job.Parameters.UNLIMITED)
{ {
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its max number of attempts.")); Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its max number of attempts."));
return Job.Result.FAILURE; return Job.Result.failure();
} }
return result; return result;
@ -101,7 +113,9 @@ class JobRunner extends Thread {
} }
private void printResult(@NonNull Job job, @NonNull Job.Result result) { private void printResult(@NonNull Job job, @NonNull Job.Result result) {
if (result == Job.Result.FAILURE) { if (result.getException() != null) {
Log.e(TAG, JobLogger.format(job, String.valueOf(id), "Job failed with a fatal exception. Crash imminent."));
} else if (result.isFailure()) {
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Job failed.")); Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Job failed."));
} else { } else {
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result)); Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result));

View File

@ -19,14 +19,17 @@ public abstract class BaseJob extends Job {
public @NonNull Result run() { public @NonNull Result run() {
try { try {
onRun(); onRun();
return Result.SUCCESS; return Result.success();
} catch (RuntimeException e) {
Log.e(TAG, "Encountered a fatal exception. Crash imminent.", e);
return Result.fatalFailure(e);
} catch (Exception e) { } catch (Exception e) {
if (onShouldRetry(e)) { if (onShouldRetry(e)) {
Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e); Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e);
return Result.RETRY; return Result.retry();
} else { } else {
Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e); Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e);
return Result.FAILURE; return Result.failure();
} }
} }
} }