Move backoff calculation into jobs.

This commit is contained in:
Greyson Parrelli
2021-01-28 09:48:09 -05:00
parent 6a45858b4a
commit 1d83729e6c
14 changed files with 115 additions and 112 deletions

View File

@@ -150,18 +150,21 @@ public abstract class Job {
public static final class Result {
private static final Result SUCCESS_NO_DATA = new Result(ResultType.SUCCESS, null, null);
private static final Result RETRY = new Result(ResultType.RETRY, null, null);
private static final Result FAILURE = new Result(ResultType.FAILURE, null, null);
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) {
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. */
@@ -171,12 +174,15 @@ public abstract class Job {
/** Job completed successfully and wants to provide some output data. */
public static Result success(@Nullable Data outputData) {
return new Result(ResultType.SUCCESS, null, outputData);
return new Result(ResultType.SUCCESS, null, outputData, INVALID_BACKOFF);
}
/** Job did not complete successfully, but it can be retried later. */
public static Result retry() {
return RETRY;
/**
* 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.*/
@@ -186,7 +192,7 @@ public abstract class Job {
/** 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);
return new Result(ResultType.FAILURE, runtimeException, null, INVALID_BACKOFF);
}
boolean isSuccess() {
@@ -209,6 +215,10 @@ public abstract class Job {
return outputData;
}
long getBackoffInterval() {
return backoffInterval;
}
@Override
public @NonNull String toString() {
switch (resultType) {
@@ -241,7 +251,6 @@ public abstract class Job {
private final long createTime;
private final long lifespan;
private final int maxAttempts;
private final long maxBackoff;
private final int maxInstancesForFactory;
private final int maxInstancesForQueue;
private final String queue;
@@ -253,7 +262,6 @@ public abstract class Job {
long createTime,
long lifespan,
int maxAttempts,
long maxBackoff,
int maxInstancesForFactory,
int maxInstancesForQueue,
@Nullable String queue,
@@ -265,7 +273,6 @@ public abstract class Job {
this.createTime = createTime;
this.lifespan = lifespan;
this.maxAttempts = maxAttempts;
this.maxBackoff = maxBackoff;
this.maxInstancesForFactory = maxInstancesForFactory;
this.maxInstancesForQueue = maxInstancesForQueue;
this.queue = queue;
@@ -290,10 +297,6 @@ public abstract class Job {
return maxAttempts;
}
long getMaxBackoff() {
return maxBackoff;
}
int getMaxInstancesForFactory() {
return maxInstancesForFactory;
}
@@ -319,14 +322,13 @@ public abstract class Job {
}
public Builder toBuilder() {
return new Builder(id, createTime, maxBackoff, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
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 maxBackoff;
private long lifespan;
private int maxAttempts;
private int maxInstancesForFactory;
@@ -341,12 +343,11 @@ public abstract class Job {
}
Builder(@NonNull String id) {
this(id, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(FeatureFlags.getDefaultMaxBackoffSeconds()), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false);
this(id, System.currentTimeMillis(), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false);
}
private Builder(@NonNull String id,
long createTime,
long maxBackoff,
long lifespan,
int maxAttempts,
int maxInstancesForFactory,
@@ -358,7 +359,6 @@ public abstract class Job {
{
this.id = id;
this.createTime = createTime;
this.maxBackoff = maxBackoff;
this.lifespan = lifespan;
this.maxAttempts = maxAttempts;
this.maxInstancesForFactory = maxInstancesForFactory;
@@ -391,15 +391,6 @@ public abstract class Job {
return this;
}
/**
* Specify the longest amount of time to wait between retries. No guarantees that this will
* be respected on API >= 26.
*/
public @NonNull Builder setMaxBackoff(long maxBackoff) {
this.maxBackoff = maxBackoff;
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,
@@ -479,7 +470,7 @@ public abstract class Job {
}
public @NonNull Parameters build() {
return new Parameters(id, createTime, lifespan, maxAttempts, maxBackoff, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
return new Parameters(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
}
}
}

View File

@@ -161,9 +161,13 @@ class JobController {
}
@WorkerThread
synchronized void onRetry(@NonNull Job job) {
synchronized void onRetry(@NonNull Job job, long backoffInterval) {
if (backoffInterval <= 0) {
throw new IllegalArgumentException("Invalid backoff interval! " + backoffInterval);
}
int nextRunAttempt = job.getRunAttempt() + 1;
long nextRunAttemptTime = calculateNextRunAttemptTime(System.currentTimeMillis(), nextRunAttempt, TimeUnit.SECONDS.toMillis(FeatureFlags.getDefaultMaxBackoffSeconds()));
long nextRunAttemptTime = System.currentTimeMillis() + backoffInterval;
String serializedData = dataSerializer.serialize(job.serialize());
jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime, serializedData);
@@ -355,7 +359,6 @@ class JobController {
job.getNextRunAttemptTime(),
job.getRunAttempt(),
job.getParameters().getMaxAttempts(),
job.getParameters().getMaxBackoff(),
job.getParameters().getLifespan(),
dataSerializer.serialize(job.serialize()),
null,
@@ -447,22 +450,10 @@ class JobController {
.setMaxAttempts(jobSpec.getMaxAttempts())
.setQueue(jobSpec.getQueueKey())
.setConstraints(Stream.of(constraintSpecs).map(ConstraintSpec::getFactoryKey).toList())
.setMaxBackoff(jobSpec.getMaxBackoff())
.setInputData(jobSpec.getSerializedInputData() != null ? dataSerializer.deserialize(jobSpec.getSerializedInputData()) : null)
.build();
}
private long calculateNextRunAttemptTime(long currentTime, int nextAttempt, long maxBackoff) {
int boundedAttempt = Math.min(nextAttempt, 30);
long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000;
long actualBackoff = Math.min(exponentialBackoff, maxBackoff);
double jitter = 0.75 + (Math.random() * 0.5);
actualBackoff = (long) (actualBackoff * jitter);
return currentTime + actualBackoff;
}
private @NonNull JobSpec mapToJobWithInputData(@NonNull JobSpec jobSpec, @NonNull Data inputData) {
return new JobSpec(jobSpec.getId(),
jobSpec.getFactoryKey(),
@@ -471,7 +462,6 @@ class JobController {
jobSpec.getNextRunAttemptTime(),
jobSpec.getRunAttempt(),
jobSpec.getMaxAttempts(),
jobSpec.getMaxBackoff(),
jobSpec.getLifespan(),
jobSpec.getSerializedData(),
dataSerializer.serialize(inputData),

View File

@@ -69,7 +69,6 @@ public class JobMigrator {
jobSpec.getNextRunAttemptTime(),
jobSpec.getRunAttempt(),
jobSpec.getMaxAttempts(),
jobSpec.getMaxBackoff(),
jobSpec.getLifespan(),
dataSerializer.serialize(updatedJobData.getData()),
jobSpec.getSerializedInputData(),

View File

@@ -53,7 +53,7 @@ class JobRunner extends Thread {
if (result.isSuccess()) {
jobController.onSuccess(job, result.getOutputData());
} else if (result.isRetry()) {
jobController.onRetry(job);
jobController.onRetry(job, result.getBackoffInterval());
job.onRetry();
} else if (result.isFailure()) {
List<Job> dependents = jobController.onFailure(job);

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.jobmanager.impl;
public final class BackoffUtil {
private BackoffUtil() {}
/**
* Simple exponential backoff with random jitter.
* @param pastAttemptCount The number of attempts that have already been made.
*
* @return The calculated backoff.
*/
public static long exponentialBackoff(int pastAttemptCount, long maxBackoff) {
if (pastAttemptCount < 1) {
throw new IllegalArgumentException("Bad attempt count! " + pastAttemptCount);
}
int boundedAttempt = Math.min(pastAttemptCount, 30);
long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000;
long actualBackoff = Math.min(exponentialBackoff, maxBackoff);
double jitter = 0.75 + (Math.random() * 0.5);
return (long) (actualBackoff * jitter);
}
}

View File

@@ -16,7 +16,6 @@ public final class JobSpec {
private final long nextRunAttemptTime;
private final int runAttempt;
private final int maxAttempts;
private final long maxBackoff;
private final long lifespan;
private final String serializedData;
private final String serializedInputData;
@@ -30,7 +29,6 @@ public final class JobSpec {
long nextRunAttemptTime,
int runAttempt,
int maxAttempts,
long maxBackoff,
long lifespan,
@NonNull String serializedData,
@Nullable String serializedInputData,
@@ -42,7 +40,6 @@ public final class JobSpec {
this.queueKey = queueKey;
this.createTime = createTime;
this.nextRunAttemptTime = nextRunAttemptTime;
this.maxBackoff = maxBackoff;
this.runAttempt = runAttempt;
this.maxAttempts = maxAttempts;
this.lifespan = lifespan;
@@ -80,10 +77,6 @@ public final class JobSpec {
return maxAttempts;
}
public long getMaxBackoff() {
return maxBackoff;
}
public long getLifespan() {
return lifespan;
}
@@ -113,7 +106,6 @@ public final class JobSpec {
nextRunAttemptTime == jobSpec.nextRunAttemptTime &&
runAttempt == jobSpec.runAttempt &&
maxAttempts == jobSpec.maxAttempts &&
maxBackoff == jobSpec.maxBackoff &&
lifespan == jobSpec.lifespan &&
isRunning == jobSpec.isRunning &&
memoryOnly == jobSpec.memoryOnly &&
@@ -126,13 +118,13 @@ public final class JobSpec {
@Override
public int hashCode() {
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, serializedData, serializedInputData, isRunning, memoryOnly);
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, lifespan, serializedData, serializedInputData, isRunning, memoryOnly);
}
@SuppressLint("DefaultLocale")
@Override
public @NonNull String toString() {
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | lifespan: %d | isRunning: %b | memoryOnly: %b",
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, isRunning, memoryOnly);
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | lifespan: %d | isRunning: %b | memoryOnly: %b",
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, lifespan, isRunning, memoryOnly);
}
}

View File

@@ -65,7 +65,6 @@ final class WorkManagerDatabase extends SQLiteOpenHelper {
0,
0,
Job.Parameters.UNLIMITED,
TimeUnit.SECONDS.toMillis(30),
TimeUnit.DAYS.toMillis(1),
dataSerializer.serialize(DataMigrator.convert(data)),
null,