Signal-Android/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java

482 wiersze
15 KiB
Java

package org.thoughtcrime.securesms.jobmanager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
/**
* A durable unit of work.
*
* Jobs have {@link Parameters} that describe the conditions upon when you'd like them to run, how
* often they should be retried, and how long they should be retried for.
*
* Never rely on a specific instance of this class being run. It can be created and destroyed as the
* job is retried. State that you want to save is persisted to a {@link Data} object in
* {@link #serialize()}. Your job is then recreated using a {@link Factory} that you register in
* {@link JobManager.Configuration.Builder#setJobFactories(Map)}, which is given the saved
* {@link Data} bundle.
*/
public abstract class Job {
private static final String TAG = Log.tag(Job.class);
private final Parameters parameters;
private int runAttempt;
private long nextRunAttemptTime;
private volatile boolean canceled;
protected Context context;
public Job(@NonNull Parameters parameters) {
this.parameters = parameters;
}
public final @NonNull String getId() {
return parameters.getId();
}
public final @NonNull Parameters getParameters() {
return parameters;
}
public final int getRunAttempt() {
return runAttempt;
}
public final long getNextRunAttemptTime() {
return nextRunAttemptTime;
}
public final @Nullable Data getInputData() {
return parameters.getInputData();
}
public final @NonNull Data requireInputData() {
return Objects.requireNonNull(parameters.getInputData());
}
/**
* This is already called by {@link JobController} during job submission, but if you ever run a
* job without submitting it to the {@link JobManager}, then you'll need to invoke this yourself.
*/
public final void setContext(@NonNull Context context) {
this.context = context;
}
/** Should only be invoked by {@link JobController} */
final void setRunAttempt(int runAttempt) {
this.runAttempt = runAttempt;
}
/** Should only be invoked by {@link JobController} */
final void setNextRunAttemptTime(long nextRunAttemptTime) {
this.nextRunAttemptTime = nextRunAttemptTime;
}
/** Should only be invoked by {@link JobController} */
final void cancel() {
this.canceled = true;
}
@WorkerThread
final void onSubmit() {
Log.i(TAG, JobLogger.format(this, "onSubmit()"));
onAdded();
}
/**
* @return True if your job has been marked as canceled while it was running, otherwise false.
* If a job sees that it has been canceled, it should make a best-effort attempt at
* stopping it's work. This job will have {@link #onFailure()} called after {@link #run()}
* has finished.
*/
public final boolean isCanceled() {
return canceled;
}
/**
* Called when the job is first submitted to the {@link JobManager}.
*/
@WorkerThread
public void onAdded() {
}
/**
* Called after a job has run and its determined that a retry is required.
*/
@WorkerThread
public void onRetry() {
}
/**
* Serialize your job state so that it can be recreated in the future.
*/
public abstract @NonNull Data serialize();
/**
* Returns the key that can be used to find the relevant factory needed to create your job.
*/
public abstract @NonNull String getFactoryKey();
/**
* Called to do your actual work.
*/
@WorkerThread
public abstract @NonNull Result run();
/**
* Called when your job has completely failed and will not be run again.
*/
@WorkerThread
public abstract void onFailure();
public interface Factory<T extends Job> {
@NonNull T create(@NonNull Parameters parameters, @NonNull Data data);
}
public static final class Result {
private static final int INVALID_BACKOFF = -1;
private static final Result SUCCESS_NO_DATA = new Result(ResultType.SUCCESS, null, null, INVALID_BACKOFF);
private static final Result FAILURE = new Result(ResultType.FAILURE, null, null, INVALID_BACKOFF);
private final ResultType resultType;
private final RuntimeException runtimeException;
private final Data outputData;
private final long backoffInterval;
private Result(@NonNull ResultType resultType, @Nullable RuntimeException runtimeException, @Nullable Data outputData, long backoffInterval) {
this.resultType = resultType;
this.runtimeException = runtimeException;
this.outputData = outputData;
this.backoffInterval = backoffInterval;
}
/** Job completed successfully. */
public static Result success() {
return SUCCESS_NO_DATA;
}
/** Job completed successfully and wants to provide some output data. */
public static Result success(@Nullable Data outputData) {
return new Result(ResultType.SUCCESS, null, outputData, INVALID_BACKOFF);
}
/**
* Job did not complete successfully, but it can be retried later.
* @param backoffInterval How long to wait before retrying
*/
public static Result retry(long backoffInterval) {
return new Result(ResultType.RETRY, null, null, backoffInterval);
}
/** 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, null, INVALID_BACKOFF);
}
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public boolean isSuccess() {
return resultType == ResultType.SUCCESS;
}
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public boolean isRetry() {
return resultType == ResultType.RETRY;
}
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public boolean isFailure() {
return resultType == ResultType.FAILURE;
}
@Nullable RuntimeException getException() {
return runtimeException;
}
@Nullable Data getOutputData() {
return outputData;
}
long getBackoffInterval() {
return backoffInterval;
}
@Override
public @NonNull String toString() {
switch (resultType) {
case SUCCESS:
case RETRY:
return resultType.toString();
case FAILURE:
if (runtimeException == null) {
return resultType.toString();
} else {
return "FATAL_FAILURE";
}
}
return "UNKNOWN?";
}
private enum ResultType {
SUCCESS, FAILURE, RETRY
}
}
public static final class Parameters {
public static final String MIGRATION_QUEUE_KEY = "MIGRATION";
public static final long IMMORTAL = -1;
public static final int UNLIMITED = -1;
private final String id;
private final long createTime;
private final long lifespan;
private final int maxAttempts;
private final int maxInstancesForFactory;
private final int maxInstancesForQueue;
private final String queue;
private final List<String> constraintKeys;
private final Data inputData;
private final boolean memoryOnly;
private Parameters(@NonNull String id,
long createTime,
long lifespan,
int maxAttempts,
int maxInstancesForFactory,
int maxInstancesForQueue,
@Nullable String queue,
@NonNull List<String> constraintKeys,
@Nullable Data inputData,
boolean memoryOnly)
{
this.id = id;
this.createTime = createTime;
this.lifespan = lifespan;
this.maxAttempts = maxAttempts;
this.maxInstancesForFactory = maxInstancesForFactory;
this.maxInstancesForQueue = maxInstancesForQueue;
this.queue = queue;
this.constraintKeys = constraintKeys;
this.inputData = inputData;
this.memoryOnly = memoryOnly;
}
@NonNull String getId() {
return id;
}
long getCreateTime() {
return createTime;
}
long getLifespan() {
return lifespan;
}
int getMaxAttempts() {
return maxAttempts;
}
int getMaxInstancesForFactory() {
return maxInstancesForFactory;
}
int getMaxInstancesForQueue() {
return maxInstancesForQueue;
}
public @Nullable String getQueue() {
return queue;
}
@NonNull List<String> getConstraintKeys() {
return constraintKeys;
}
@Nullable Data getInputData() {
return inputData;
}
boolean isMemoryOnly() {
return memoryOnly;
}
public Builder toBuilder() {
return new Builder(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
}
public static final class Builder {
private String id;
private long createTime;
private long lifespan;
private int maxAttempts;
private int maxInstancesForFactory;
private int maxInstancesForQueue;
private String queue;
private List<String> constraintKeys;
private Data inputData;
private boolean memoryOnly;
public Builder() {
this(UUID.randomUUID().toString());
}
Builder(@NonNull String id) {
this(id, System.currentTimeMillis(), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false);
}
private Builder(@NonNull String id,
long createTime,
long lifespan,
int maxAttempts,
int maxInstancesForFactory,
int maxInstancesForQueue,
@Nullable String queue,
@NonNull List<String> constraintKeys,
@Nullable Data inputData,
boolean memoryOnly)
{
this.id = id;
this.createTime = createTime;
this.lifespan = lifespan;
this.maxAttempts = maxAttempts;
this.maxInstancesForFactory = maxInstancesForFactory;
this.maxInstancesForQueue = maxInstancesForQueue;
this.queue = queue;
this.constraintKeys = constraintKeys;
this.inputData = inputData;
this.memoryOnly = memoryOnly;
}
/** Should only be invoked by {@link JobController} */
Builder setCreateTime(long createTime) {
this.createTime = createTime;
return this;
}
/**
* Specify the amount of time this job is allowed to be retried. Defaults to {@link #IMMORTAL}.
*/
public @NonNull Builder setLifespan(long lifespan) {
this.lifespan = lifespan;
return this;
}
/**
* Specify the maximum number of times you want to attempt this job. Defaults to 1.
*/
public @NonNull Builder setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
return this;
}
/**
* Specify the maximum number of instances you'd want of this job at any given time, as
* determined by the job's factory key. If enqueueing this job would put it over that limit,
* it will be ignored.
*
* This property is ignored if the job is submitted as part of a {@link JobManager.Chain}.
*
* Defaults to {@link #UNLIMITED}.
*/
public @NonNull Builder setMaxInstancesForFactory(int maxInstancesForFactory) {
this.maxInstancesForFactory = maxInstancesForFactory;
return this;
}
/**
* Specify the maximum number of instances you'd want of this job at any given time, as
* determined by the job's factory key and queue key. If enqueueing this job would put it over
* that limit, it will be ignored.
*
* This property is ignored if the job is submitted as part of a {@link JobManager.Chain}, or
* if the job has no queue key.
*
* Defaults to {@link #UNLIMITED}.
*/
public @NonNull Builder setMaxInstancesForQueue(int maxInstancesForQueue) {
this.maxInstancesForQueue = maxInstancesForQueue;
return this;
}
/**
* Specify a string representing a queue. All jobs within the same queue are run in a
* serialized fashion -- one after the other, in order of insertion. Failure of a job earlier
* in the queue has no impact on the execution of jobs later in the queue.
*/
public @NonNull Builder setQueue(@Nullable String queue) {
this.queue = queue;
return this;
}
/**
* Add a constraint via the key that was used to register its factory in
* {@link JobManager.Configuration)};
*/
public @NonNull Builder addConstraint(@NonNull String constraintKey) {
constraintKeys.add(constraintKey);
return this;
}
/**
* Set constraints via the key that was used to register its factory in
* {@link JobManager.Configuration)};
*/
public @NonNull Builder setConstraints(@NonNull List<String> constraintKeys) {
this.constraintKeys.clear();
this.constraintKeys.addAll(constraintKeys);
return this;
}
/**
* Specify whether or not you want this job to only live in memory. If true, this job will
* *not* survive application death. This defaults to false, and should be used with care.
*
* Defaults to false.
*/
public @NonNull Builder setMemoryOnly(boolean memoryOnly) {
this.memoryOnly = memoryOnly;
return this;
}
/**
* Sets the input data that will be made availabe to the job when it is run.
* Should only be set by {@link JobController}.
*/
@NonNull Builder setInputData(@Nullable Data inputData) {
this.inputData = inputData;
return this;
}
public @NonNull Parameters build() {
return new Parameters(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
}
}
}
}