Create system for job migrations.

This commit is contained in:
Greyson Parrelli 2019-08-21 22:14:38 -04:00 committed by Alan Evans
parent 9580bb0a38
commit af42d5b671
12 changed files with 388 additions and 3 deletions

View file

@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JobMigrator;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FastJobStorage;
@ -222,6 +223,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(this), 1, JobManagerFactories.getJobMigrations()))
.build());
}

View file

@ -141,6 +141,39 @@ public class JobDatabase extends Database {
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
}
public synchronized void updateJobs(@NonNull List<JobSpec> jobs) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (JobSpec job : jobs) {
ContentValues values = new ContentValues();
values.put(Jobs.JOB_SPEC_ID, job.getId());
values.put(Jobs.FACTORY_KEY, job.getFactoryKey());
values.put(Jobs.QUEUE_KEY, job.getQueueKey());
values.put(Jobs.CREATE_TIME, job.getCreateTime());
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
values.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
values.put(Jobs.LIFESPAN, job.getLifespan());
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
values.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
String query = Jobs.JOB_SPEC_ID + " = ?";
String[] args = new String[]{ job.getId() };
db.update(Jobs.TABLE_NAME, values, query, args);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();

View file

@ -63,7 +63,6 @@ class JobController {
@WorkerThread
synchronized void init() {
jobStorage.init();
jobStorage.updateAllJobsToBePending();
notifyAll();
}

View file

@ -4,6 +4,7 @@ import android.app.Application;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.jobmanager.workmanager.WorkManagerMigrator;
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.ArrayList;
import java.util.Collections;
@ -58,6 +60,12 @@ public class JobManager implements ConstraintObserver.Notifier {
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
}
JobStorage jobStorage = configuration.getJobStorage();
jobStorage.init();
int latestVersion = configuration.getJobMigrator().migrate(jobStorage, configuration.getDataSerializer());
TextSecurePreferences.setJobManagerVersion(application, latestVersion);
jobController.init();
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
@ -214,6 +222,7 @@ public class JobManager implements ConstraintObserver.Notifier {
private final List<ConstraintObserver> constraintObservers;
private final Data.Serializer dataSerializer;
private final JobStorage jobStorage;
private final JobMigrator jobMigrator;
private Configuration(int jobThreadCount,
@NonNull ExecutorFactory executorFactory,
@ -221,7 +230,8 @@ public class JobManager implements ConstraintObserver.Notifier {
@NonNull ConstraintInstantiator constraintInstantiator,
@NonNull List<ConstraintObserver> constraintObservers,
@NonNull Data.Serializer dataSerializer,
@NonNull JobStorage jobStorage)
@NonNull JobStorage jobStorage,
@NonNull JobMigrator jobMigrator)
{
this.executorFactory = executorFactory;
this.jobThreadCount = jobThreadCount;
@ -230,6 +240,7 @@ public class JobManager implements ConstraintObserver.Notifier {
this.constraintObservers = constraintObservers;
this.dataSerializer = dataSerializer;
this.jobStorage = jobStorage;
this.jobMigrator = jobMigrator;
}
int getJobThreadCount() {
@ -261,6 +272,10 @@ public class JobManager implements ConstraintObserver.Notifier {
return jobStorage;
}
@NonNull JobMigrator getJobMigrator() {
return jobMigrator;
}
public static class Builder {
private ExecutorFactory executorFactory = new DefaultExecutorFactory();
@ -270,6 +285,7 @@ public class JobManager implements ConstraintObserver.Notifier {
private List<ConstraintObserver> constraintObservers = new ArrayList<>();
private Data.Serializer dataSerializer = new JsonDataSerializer();
private JobStorage jobStorage = null;
private JobMigrator jobMigrator = null;
public @NonNull Builder setJobThreadCount(int jobThreadCount) {
this.jobThreadCount = jobThreadCount;
@ -306,6 +322,11 @@ public class JobManager implements ConstraintObserver.Notifier {
return this;
}
public @NonNull Builder setJobMigrator(@NonNull JobMigrator jobMigrator) {
this.jobMigrator = jobMigrator;
return this;
}
public @NonNull Configuration build() {
return new Configuration(jobThreadCount,
executorFactory,
@ -313,7 +334,8 @@ public class JobManager implements ConstraintObserver.Notifier {
new ConstraintInstantiator(constraintFactories),
new ArrayList<>(constraintObservers),
dataSerializer,
jobStorage);
jobStorage,
jobMigrator);
}
}
}

View file

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.jobmanager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Create a subclass of this to perform a migration on persisted {@link Job}s. A migration targets
* a specific end version, and the assumption is that it can migrate jobs to that end version from
* the previous version. The class will be provided a bundle of job data for each persisted job and
* give back an updated version (if applicable).
*/
public abstract class JobMigration {
private final int endVersion;
protected JobMigration(int endVersion) {
this.endVersion = endVersion;
}
/**
* Given a bundle of job data, return a bundle of job data that should be used in place of it.
* You may obviously return the same object if you don't wish to change it.
*/
protected abstract @NonNull JobData migrate(@NonNull JobData jobData);
int getEndVersion() {
return endVersion;
}
protected static class JobData {
private final String factoryKey;
private final String queueKey;
private final Data data;
JobData(@NonNull String factoryKey, @Nullable String queueKey, @NonNull Data data) {
this.factoryKey = factoryKey;
this.queueKey = queueKey;
this.data = data;
}
protected @NonNull JobData withQueueKey(@Nullable String newQueueKey) {
return new JobData(factoryKey, newQueueKey, data);
}
protected @NonNull JobData withData(@NonNull Data newData) {
return new JobData(factoryKey, queueKey, newData);
}
public @NonNull String getFactoryKey() {
return factoryKey;
}
public @Nullable String getQueueKey() {
return queueKey;
}
public @NonNull Data getData() {
return data;
}
}
}

View file

@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.jobmanager;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@SuppressLint("UseSparseArrays")
public class JobMigrator {
private static final String TAG = Log.tag(JobMigrator.class);
private final int lastSeenVersion;
private final int currentVersion;
private final Map<Integer, JobMigration> migrations;
public JobMigrator(int lastSeenVersion, int currentVersion, @NonNull List<JobMigration> migrations) {
this.lastSeenVersion = lastSeenVersion;
this.currentVersion = currentVersion;
this.migrations = new HashMap<>();
if (migrations.size() != currentVersion - 1) {
throw new AssertionError("You must have a migration for every version!");
}
for (int i = 0; i < migrations.size(); i++) {
JobMigration migration = migrations.get(i);
if (migration.getEndVersion() != i + 2) {
throw new AssertionError("Missing migration for version " + (i + 2) + "!");
}
this.migrations.put(migration.getEndVersion(), migrations.get(i));
}
}
/**
* @return The version that has been migrated to.
*/
int migrate(@NonNull JobStorage jobStorage, @NonNull Data.Serializer dataSerializer) {
List<JobSpec> jobSpecs = jobStorage.getAllJobSpecs();
for (int i = lastSeenVersion; i < currentVersion; i++) {
Log.i(TAG, "Migrating from " + i + " to " + (i + 1));
ListIterator<JobSpec> iter = jobSpecs.listIterator();
JobMigration migration = migrations.get(i + 1);
assert migration != null;
while (iter.hasNext()) {
JobSpec jobSpec = iter.next();
Data data = dataSerializer.deserialize(jobSpec.getSerializedData());
JobData originalJobData = new JobData(jobSpec.getFactoryKey(), jobSpec.getQueueKey(), data);
JobData updatedJobData = migration.migrate(originalJobData);
JobSpec updatedJobSpec = new JobSpec(jobSpec.getId(),
jobSpec.getFactoryKey(),
updatedJobData.getQueueKey(),
jobSpec.getCreateTime(),
jobSpec.getNextRunAttemptTime(),
jobSpec.getRunAttempt(),
jobSpec.getMaxAttempts(),
jobSpec.getMaxBackoff(),
jobSpec.getLifespan(),
jobSpec.getMaxInstances(),
dataSerializer.serialize(updatedJobData.getData()),
jobSpec.isRunning());
iter.set(updatedJobSpec);
}
}
jobStorage.updateJobs(jobSpecs);
return currentVersion;
}
}

View file

@ -35,6 +35,9 @@ public interface JobStorage {
@WorkerThread
void updateAllJobsToBePending();
@WorkerThread
void updateJobs(@NonNull List<JobSpec> jobSpecs);
@WorkerThread
void deleteJob(@NonNull String id);

View file

@ -212,6 +212,23 @@ public class FastJobStorage implements JobStorage {
}
}
@Override
public void updateJobs(@NonNull List<JobSpec> jobSpecs) {
jobDatabase.updateJobs(jobSpecs);
Map<String, JobSpec> updates = Stream.of(jobSpecs).collect(Collectors.toMap(JobSpec::getId));
ListIterator<JobSpec> iter = jobs.listIterator();
while (iter.hasNext()) {
JobSpec existing = iter.next();
JobSpec update = updates.get(existing.getId());
if (update != null) {
iter.set(update);
}
}
}
@Override
public synchronized void deleteJob(@NonNull String jobId) {
deleteJobs(Collections.singletonList(jobId));

View file

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.Constraint;
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobMigration;
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -18,6 +19,7 @@ import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -102,4 +104,8 @@ public final class JobManagerFactories {
new NetworkConstraintObserver(application),
new SqlCipherMigrationConstraintObserver());
}
public static List<JobMigration> getJobMigrations() {
return Collections.emptyList();
}
}

View file

@ -187,6 +187,8 @@ public class TextSecurePreferences {
private static final String SEEN_CAMERA_FIRST_TOOLTIP = "pref_seen_camera_first_tooltip";
private static final String JOB_MANAGER_VERSION = "pref_job_manager_version";
public static boolean isScreenLockEnabled(@NonNull Context context) {
return getBooleanPreference(context, SCREEN_LOCK, false);
}
@ -1118,6 +1120,14 @@ public class TextSecurePreferences {
return getBooleanPreference(context, SEEN_CAMERA_FIRST_TOOLTIP, false);
}
public static void setJobManagerVersion(Context context, int version) {
setIntegerPrefrence(context, JOB_MANAGER_VERSION, version);
}
public static int getJobManagerVersion(Context contex) {
return getIntegerPreference(contex, JOB_MANAGER_VERSION, 1);
}
public static void setBooleanPreference(Context context, String key, boolean value) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
}

View file

@ -0,0 +1,106 @@
package org.thoughtcrime.securesms.jobmanager;
import androidx.annotation.NonNull;
import org.junit.BeforeClass;
import org.junit.Test;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JobMigratorTest {
@BeforeClass
public static void init() {
Log.initialize(mock(Log.Logger.class));
}
@Test(expected = AssertionError.class)
public void JobMigrator_crashWhenTooFewMigrations() {
new JobMigrator(1, 2, Collections.emptyList());
}
@Test(expected = AssertionError.class)
public void JobMigrator_crashWhenTooManyMigrations() {
new JobMigrator(1, 2, Arrays.asList(new EmptyMigration(2), new EmptyMigration(3)));
}
@Test(expected = AssertionError.class)
public void JobMigrator_crashWhenSkippingMigrations() {
new JobMigrator(1, 3, Arrays.asList(new EmptyMigration(2), new EmptyMigration(4)));
}
@Test
public void JobMigrator_properInitialization() {
new JobMigrator(1, 3, Arrays.asList(new EmptyMigration(2), new EmptyMigration(3)));
}
@Test
public void migrate_callsAppropriateMigrations_fullSet() {
JobMigration migration1 = spy(new EmptyMigration(2));
JobMigration migration2 = spy(new EmptyMigration(3));
JobMigrator subject = new JobMigrator(1, 3, Arrays.asList(migration1, migration2));
int version = subject.migrate(simpleJobStorage(), mock(Data.Serializer.class));
assertEquals(3, version);
verify(migration1).migrate(any());
verify(migration2).migrate(any());
}
@Test
public void migrate_callsAppropriateMigrations_subset() {
JobMigration migration1 = spy(new EmptyMigration(2));
JobMigration migration2 = spy(new EmptyMigration(3));
JobMigrator subject = new JobMigrator(2, 3, Arrays.asList(migration1, migration2));
int version = subject.migrate(simpleJobStorage(), mock(Data.Serializer.class));
assertEquals(3, version);
verify(migration1, never()).migrate(any());
verify(migration2).migrate(any());
}
@Test
public void migrate_callsAppropriateMigrations_none() {
JobMigration migration1 = spy(new EmptyMigration(2));
JobMigration migration2 = spy(new EmptyMigration(3));
JobMigrator subject = new JobMigrator(3, 3, Arrays.asList(migration1, migration2));
int version = subject.migrate(simpleJobStorage(), mock(Data.Serializer.class));
assertEquals(3, version);
verify(migration1, never()).migrate(any());
verify(migration2, never()).migrate(any());
}
private static JobStorage simpleJobStorage() {
JobStorage jobStorage = mock(JobStorage.class);
when(jobStorage.getAllJobSpecs()).thenReturn(new ArrayList<>(Collections.singletonList(new JobSpec("1", "f1", null, 1, 1, 1, 1, 1, 1, 1, "", false))));
return jobStorage;
}
private static class EmptyMigration extends JobMigration {
protected EmptyMigration(int endVersion) {
super(endVersion);
}
@Override
protected @NonNull JobData migrate(@NonNull JobData jobData) {
return jobData;
}
}
}

View file

@ -101,6 +101,43 @@ public class FastJobStorageTest {
assertFalse(subject.getJobSpec("2").isRunning());
}
@Test
public void updateJobs_writesToDatabase() {
JobDatabase database = noopDatabase();
FastJobStorage subject = new FastJobStorage(database);
List<JobSpec> jobs = Collections.emptyList();
subject.updateJobs(jobs);
verify(database).updateJobs(jobs);
}
@Test
public void updateJobs_updatesAllFields() {
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, false),
Collections.emptyList(),
Collections.emptyList());
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, false),
Collections.emptyList(),
Collections.emptyList());
FullSpec fullSpec3 = new FullSpec(new JobSpec("3", "f3", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, false),
Collections.emptyList(),
Collections.emptyList());
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2, fullSpec3)));
JobSpec update1 = new JobSpec("1", "g1", "q1", 2, 2, 2, 2, 2, 2, 2, "abc", true);
JobSpec update2 = new JobSpec("2", "g2", "q2", 3, 3, 3, 3, 3, 3, 3, "def", true);
subject.init();
subject.updateJobs(Arrays.asList(update1, update2));
assertEquals(update1, subject.getJobSpec("1"));
assertEquals(update2, subject.getJobSpec("2"));
assertEquals(fullSpec3.getJobSpec(), subject.getJobSpec("3"));
}
@Test
public void updateJobRunningState_writesToDatabase() {
JobDatabase database = noopDatabase();