diff --git a/src/org/thoughtcrime/securesms/jobmanager/Job.java b/src/org/thoughtcrime/securesms/jobmanager/Job.java index dfcc087208..0cb525d300 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/src/org/thoughtcrime/securesms/jobmanager/Job.java @@ -125,8 +125,59 @@ public abstract class Job { @NonNull T create(@NonNull Parameters parameters, @NonNull Data data); } - public enum Result { - SUCCESS, FAILURE, RETRY + public static final class Result { + + 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 { diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java b/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java index 249b4c1c60..afc89d8011 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java +++ b/src/org/thoughtcrime/securesms/jobmanager/JobRunner.java @@ -12,6 +12,14 @@ import org.thoughtcrime.securesms.util.WakeLockUtil; import java.util.List; 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 { private static final String TAG = JobRunner.class.getSimpleName(); @@ -32,25 +40,28 @@ class JobRunner extends Thread { @Override public synchronized void run() { + //noinspection InfiniteLoopStatement while (true) { Job job = jobController.pullNextEligibleJobForExecution(); Job.Result result = run(job); jobController.onJobFinished(job); - switch (result) { - case SUCCESS: - jobController.onSuccess(job); - break; - case RETRY: - jobController.onRetry(job); - job.onRetry(); - break; - case FAILURE: - List dependents = jobController.onFailure(job); - job.onCanceled(); - Stream.of(dependents).forEach(Job::onCanceled); - break; + if (result.isSuccess()) { + jobController.onSuccess(job); + } else if (result.isRetry()) { + jobController.onRetry(job); + job.onRetry(); + } else if (result.isFailure()) { + List dependents = jobController.onFailure(job); + job.onCanceled(); + Stream.of(dependents).forEach(Job::onCanceled); + + if (result.getException() != null) { + throw result.getException(); + } + } else { + throw new AssertionError("Invalid job result!"); } } } @@ -60,7 +71,7 @@ class JobRunner extends Thread { if (isJobExpired(job)) { 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; @@ -71,7 +82,7 @@ class JobRunner extends Thread { result = job.run(); } catch (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 { if (wakeLock != null) { WakeLockUtil.release(wakeLock, job.getId()); @@ -80,11 +91,12 @@ class JobRunner extends Thread { 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) { 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; @@ -101,7 +113,9 @@ class JobRunner extends Thread { } 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.")); } else { Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result)); diff --git a/src/org/thoughtcrime/securesms/jobs/BaseJob.java b/src/org/thoughtcrime/securesms/jobs/BaseJob.java index befe91d651..fd7cc1be95 100644 --- a/src/org/thoughtcrime/securesms/jobs/BaseJob.java +++ b/src/org/thoughtcrime/securesms/jobs/BaseJob.java @@ -19,14 +19,17 @@ public abstract class BaseJob extends Job { public @NonNull Result run() { try { 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) { if (onShouldRetry(e)) { Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e); - return Result.RETRY; + return Result.retry(); } else { Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e); - return Result.FAILURE; + return Result.failure(); } } }