Move constraint filtering down into JobStorage to improve perf.

This commit is contained in:
Greyson Parrelli 2024-07-18 13:59:24 -04:00 committed by Nicholas Tinsley
parent 36dface175
commit 06d475fb6e
11 changed files with 187 additions and 228 deletions

View file

@ -32,20 +32,20 @@ class JobManagerPerformanceTests {
@Test @Test
fun testPerformance_singleQueue() { fun testPerformance_singleQueue() {
runTest(2000) { TestJob(queue = "queue") } runTest("singleQueue", 2000) { TestJob(queue = "queue") }
} }
@Test @Test
fun testPerformance_fourQueues() { fun testPerformance_fourQueues() {
runTest(2000) { TestJob(queue = "queue-${Random.nextInt(1, 5)}") } runTest("fourQueues", 2000) { TestJob(queue = "queue-${Random.nextInt(1, 5)}") }
} }
@Test @Test
fun testPerformance_noQueues() { fun testPerformance_noQueues() {
runTest(2000) { TestJob(queue = null) } runTest("noQueues", 2000) { TestJob(queue = null) }
} }
private fun runTest(count: Int, jobCreator: () -> TestJob) { private fun runTest(name: String, count: Int, jobCreator: () -> TestJob) {
val context = AppDependencies.application val context = AppDependencies.application
val jobManager = testJobManager(context) val jobManager = testJobManager(context)
@ -66,17 +66,17 @@ class JobManagerPerformanceTests {
eventTimer.emit("job") eventTimer.emit("job")
latch.countDown() latch.countDown()
if (latch.count % 100 == 0L) { if (latch.count % 100 == 0L) {
Log.d(TAG, "Finished ${count - latch.count}/$count jobs") Log.d(TAG, "[$name] Finished ${count - latch.count}/$count jobs")
} }
} }
} }
Log.i(TAG, "Adding jobs...") Log.i(TAG, "[$name] Adding jobs...")
jobManager.addAll((1..count).map { jobCreator() }) jobManager.addAll((1..count).map { jobCreator() })
Log.i(TAG, "Waiting for jobs to complete...") Log.i(TAG, "[$name] Waiting for jobs to complete...")
latch.await() latch.await()
Log.i(TAG, "Jobs complete!") Log.i(TAG, "[$name] Jobs complete!")
Log.i(TAG, eventTimer.stop().summary) Log.i(TAG, eventTimer.stop().summary)
} }

View file

@ -197,28 +197,6 @@ class JobDatabase(
.readToList { it.toJobSpec() } .readToList { it.toJobSpec() }
} }
@Synchronized
fun getJobSpecsByKeys(keys: Collection<String>): List<JobSpec> {
if (keys.isEmpty()) {
return emptyList()
}
val output: MutableList<JobSpec> = ArrayList(keys.size)
for (query in SqlUtil.buildCollectionQuery(Jobs.JOB_SPEC_ID, keys)) {
readableDatabase
.select()
.from(Jobs.TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.forEach {
output += it.toJobSpec()
}
}
return output
}
@Synchronized @Synchronized
fun getMostEligibleJobInQueue(queue: String): JobSpec? { fun getMostEligibleJobInQueue(queue: String): JobSpec? {
return readableDatabase return readableDatabase

View file

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
import org.thoughtcrime.securesms.jobs.MinimalJobSpec;
import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.Debouncer;
import java.util.ArrayList; import java.util.ArrayList;
@ -319,7 +320,7 @@ class JobController {
* When the job returned from this method has been run, you must call {@link #onJobFinished(Job)}. * When the job returned from this method has been run, you must call {@link #onJobFinished(Job)}.
*/ */
@WorkerThread @WorkerThread
synchronized @NonNull Job pullNextEligibleJobForExecution(@NonNull JobPredicate predicate) { synchronized @NonNull Job pullNextEligibleJobForExecution(@NonNull Predicate<MinimalJobSpec> predicate) {
try { try {
Job job; Job job;
@ -479,24 +480,27 @@ class JobController {
} }
@WorkerThread @WorkerThread
private @Nullable Job getNextEligibleJobForExecution(@NonNull JobPredicate predicate) { private @Nullable Job getNextEligibleJobForExecution(@NonNull Predicate<MinimalJobSpec> predicate) {
List<JobSpec> jobSpecs = Stream.of(jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis())) JobSpec jobSpec = jobStorage.getNextEligibleJob(System.currentTimeMillis(), minimalJobSpec -> {
.filter(predicate::shouldRun) if (!predicate.test(minimalJobSpec)) {
.toList(); return false;
}
for (JobSpec jobSpec : jobSpecs) { List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(minimalJobSpec.getId());
List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
List<Constraint> constraints = Stream.of(constraintSpecs) List<Constraint> constraints = Stream.of(constraintSpecs)
.map(ConstraintSpec::getFactoryKey) .map(ConstraintSpec::getFactoryKey)
.map(constraintInstantiator::instantiate) .map(constraintInstantiator::instantiate)
.toList(); .toList();
if (Stream.of(constraints).allMatch(Constraint::isMet)) { return Stream.of(constraints).allMatch(Constraint::isMet);
return createJob(jobSpec, constraintSpecs); });
}
if (jobSpec == null) {
return null;
} }
return null; List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
return createJob(jobSpec, constraintSpecs);
} }
private @NonNull Job createJob(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) { private @NonNull Job createJob(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) {

View file

@ -15,6 +15,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory; import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
import org.thoughtcrime.securesms.jobs.MinimalJobSpec;
import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -46,6 +47,8 @@ public class JobManager implements ConstraintObserver.Notifier {
public static final int CURRENT_VERSION = 12; public static final int CURRENT_VERSION = 12;
private static final Predicate<MinimalJobSpec> NO_PREDICATE = spec -> true;
private final Application application; private final Application application;
private final Configuration configuration; private final Configuration configuration;
private final Executor executor; private final Executor executor;
@ -109,10 +112,10 @@ public class JobManager implements ConstraintObserver.Notifier {
int id = 0; int id = 0;
for (int i = 0; i < configuration.getJobThreadCount(); i++) { for (int i = 0; i < configuration.getJobThreadCount(); i++) {
new JobRunner(application, ++id, jobController, JobPredicate.NONE).start(); new JobRunner(application, ++id, jobController, NO_PREDICATE).start();
} }
for (JobPredicate predicate : configuration.getReservedJobRunners()) { for (Predicate<MinimalJobSpec> predicate : configuration.getReservedJobRunners()) {
new JobRunner(application, ++id, jobController, predicate).start(); new JobRunner(application, ++id, jobController, predicate).start();
} }
@ -586,7 +589,7 @@ public class JobManager implements ConstraintObserver.Notifier {
private final JobStorage jobStorage; private final JobStorage jobStorage;
private final JobMigrator jobMigrator; private final JobMigrator jobMigrator;
private final JobTracker jobTracker; private final JobTracker jobTracker;
private final List<JobPredicate> reservedJobRunners; private final List<Predicate<MinimalJobSpec>> reservedJobRunners;
private Configuration(int jobThreadCount, private Configuration(int jobThreadCount,
@NonNull ExecutorFactory executorFactory, @NonNull ExecutorFactory executorFactory,
@ -596,7 +599,7 @@ public class JobManager implements ConstraintObserver.Notifier {
@NonNull JobStorage jobStorage, @NonNull JobStorage jobStorage,
@NonNull JobMigrator jobMigrator, @NonNull JobMigrator jobMigrator,
@NonNull JobTracker jobTracker, @NonNull JobTracker jobTracker,
@NonNull List<JobPredicate> reservedJobRunners) @NonNull List<Predicate<MinimalJobSpec>> reservedJobRunners)
{ {
this.executorFactory = executorFactory; this.executorFactory = executorFactory;
this.jobThreadCount = jobThreadCount; this.jobThreadCount = jobThreadCount;
@ -642,7 +645,7 @@ public class JobManager implements ConstraintObserver.Notifier {
return jobTracker; return jobTracker;
} }
@NonNull List<JobPredicate> getReservedJobRunners() { @NonNull List<Predicate<MinimalJobSpec>> getReservedJobRunners() {
return reservedJobRunners; return reservedJobRunners;
} }
@ -656,14 +659,14 @@ public class JobManager implements ConstraintObserver.Notifier {
private JobStorage jobStorage = null; private JobStorage jobStorage = null;
private JobMigrator jobMigrator = null; private JobMigrator jobMigrator = null;
private JobTracker jobTracker = new JobTracker(); private JobTracker jobTracker = new JobTracker();
private List<JobPredicate> reservedJobRunners = new ArrayList<>(); private List<Predicate<MinimalJobSpec>> reservedJobRunners = new ArrayList<>();
public @NonNull Builder setJobThreadCount(int jobThreadCount) { public @NonNull Builder setJobThreadCount(int jobThreadCount) {
this.jobThreadCount = jobThreadCount; this.jobThreadCount = jobThreadCount;
return this; return this;
} }
public @NonNull Builder addReservedJobRunner(@NonNull JobPredicate predicate) { public @NonNull Builder addReservedJobRunner(@NonNull Predicate<MinimalJobSpec> predicate) {
this.reservedJobRunners.add(predicate); this.reservedJobRunners.add(predicate);
return this; return this;
} }

View file

@ -1,11 +0,0 @@
package org.thoughtcrime.securesms.jobmanager;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
public interface JobPredicate {
JobPredicate NONE = jobSpec -> true;
boolean shouldRun(@NonNull JobSpec jobSpec);
}

View file

@ -8,10 +8,12 @@ import androidx.annotation.NonNull;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.jobs.MinimalJobSpec;
import org.thoughtcrime.securesms.util.WakeLockUtil; import org.thoughtcrime.securesms.util.WakeLockUtil;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/** /**
* A thread that constantly checks for available {@link Job}s owned by the {@link JobController}. * A thread that constantly checks for available {@link Job}s owned by the {@link JobController}.
@ -30,9 +32,9 @@ class JobRunner extends Thread {
private final Application application; private final Application application;
private final int id; private final int id;
private final JobController jobController; private final JobController jobController;
private final JobPredicate jobPredicate; private final Predicate<MinimalJobSpec> jobPredicate;
JobRunner(@NonNull Application application, int id, @NonNull JobController jobController, @NonNull JobPredicate predicate) { JobRunner(@NonNull Application application, int id, @NonNull JobController jobController, @NonNull Predicate<MinimalJobSpec> predicate) {
super("signal-JobRunner-" + id); super("signal-JobRunner-" + id);
this.application = application; this.application = application;

View file

@ -1,18 +1,16 @@
package org.thoughtcrime.securesms.jobmanager.impl; package org.thoughtcrime.securesms.jobmanager.impl;
import androidx.annotation.NonNull; import org.thoughtcrime.securesms.jobs.MinimalJobSpec;
import org.thoughtcrime.securesms.jobmanager.JobPredicate;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
/** /**
* A {@link JobPredicate} that will only run jobs with the provided factory keys. * A {@link Predicate} that will only run jobs with the provided factory keys.
*/ */
public final class FactoryJobPredicate implements JobPredicate { public final class FactoryJobPredicate implements Predicate<MinimalJobSpec> {
private final Set<String> factories; private final Set<String> factories;
@ -21,7 +19,7 @@ public final class FactoryJobPredicate implements JobPredicate {
} }
@Override @Override
public boolean shouldRun(@NonNull JobSpec jobSpec) { public boolean test(MinimalJobSpec minimalJobSpec) {
return factories.contains(jobSpec.getFactoryKey()); return factories.contains(minimalJobSpec.getFactoryKey());
} }
} }

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.jobmanager.persistence package org.thoughtcrime.securesms.jobmanager.persistence
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import org.thoughtcrime.securesms.jobs.MinimalJobSpec
import java.util.function.Predicate import java.util.function.Predicate
interface JobStorage { interface JobStorage {
@ -17,7 +18,7 @@ interface JobStorage {
fun getAllMatchingFilter(predicate: Predicate<JobSpec>): List<JobSpec> fun getAllMatchingFilter(predicate: Predicate<JobSpec>): List<JobSpec>
@WorkerThread @WorkerThread
fun getPendingJobsWithNoDependenciesInCreatedOrder(currentTime: Long): List<JobSpec> fun getNextEligibleJob(currentTime: Long, filter: (MinimalJobSpec) -> Boolean): JobSpec?
@WorkerThread @WorkerThread
fun getJobsInQueue(queue: String): List<JobSpec> fun getJobsInQueue(queue: String): List<JobSpec>

View file

@ -27,7 +27,7 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage {
/** /**
* We keep a set of job specs in memory to facilitate fast retrieval. This is important because the most common job storage pattern is * We keep a set of job specs in memory to facilitate fast retrieval. This is important because the most common job storage pattern is
* [getPendingJobsWithNoDependenciesInCreatedOrder], which needs to return full specs. * [getNextEligibleJob], which needs to return full specs.
*/ */
private val jobSpecCache: LRUCache<String, JobSpec> = LRUCache(JOB_CACHE_LIMIT) private val jobSpecCache: LRUCache<String, JobSpec> = LRUCache(JOB_CACHE_LIMIT)
@ -41,7 +41,7 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage {
/** We keep every dependency in memory, since there aren't that many, and managing a limited subset would be very complicated. */ /** We keep every dependency in memory, since there aren't that many, and managing a limited subset would be very complicated. */
private val dependenciesByJobId: MutableMap<String, MutableList<DependencySpec>> = hashMapOf() private val dependenciesByJobId: MutableMap<String, MutableList<DependencySpec>> = hashMapOf()
/** The list of jobs eligible to be returned from [getPendingJobsWithNoDependenciesInCreatedOrder], kept sorted in the appropriate order. */ /** The list of jobs eligible to be returned from [getNextEligibleJob], kept sorted in the appropriate order. */
private val eligibleJobs: TreeSet<MinimalJobSpec> = TreeSet(EligibleMinJobComparator) private val eligibleJobs: TreeSet<MinimalJobSpec> = TreeSet(EligibleMinJobComparator)
/** All migration-related jobs, kept in the appropriate order. */ /** All migration-related jobs, kept in the appropriate order. */
@ -124,16 +124,16 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage {
} }
@Synchronized @Synchronized
override fun getPendingJobsWithNoDependenciesInCreatedOrder(currentTime: Long): List<JobSpec> { override fun getNextEligibleJob(currentTime: Long, filter: (MinimalJobSpec) -> Boolean): JobSpec? {
val stopwatch = debugStopwatch("get-pending") val stopwatch = debugStopwatch("get-pending")
val migrationJob: MinimalJobSpec? = migrationJobs.firstOrNull() val migrationJob: MinimalJobSpec? = migrationJobs.firstOrNull()
return if (migrationJob != null && !migrationJob.isRunning && migrationJob.hasEligibleRunTime(currentTime)) { return if (migrationJob != null && !migrationJob.isRunning && migrationJob.hasEligibleRunTime(currentTime)) {
listOf(migrationJob.toJobSpec()) migrationJob.toJobSpec()
} else if (migrationJob != null) { } else if (migrationJob != null) {
emptyList() null
} else { } else {
val minJobs: List<MinimalJobSpec> = eligibleJobs eligibleJobs
.asSequence() .asSequence()
.filter { job -> .filter { job ->
// Filter out all jobs with unmet dependencies // Filter out all jobs with unmet dependencies
@ -141,9 +141,8 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage {
} }
.filterNot { it.isRunning } .filterNot { it.isRunning }
.filter { job -> job.hasEligibleRunTime(currentTime) } .filter { job -> job.hasEligibleRunTime(currentTime) }
.toList() .firstOrNull(filter)
?.toJobSpec()
getFullJobs(minJobs)
}.also { }.also {
stopwatch?.stop(TAG) stopwatch?.stop(TAG)
} }
@ -521,21 +520,6 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage {
} }
} }
private fun getFullJobs(minJobs: Collection<MinimalJobSpec>): List<JobSpec> {
val requestedKeys = minJobs.map { it.id }.toSet()
val cachedKeys = jobSpecCache.keys.intersect(requestedKeys)
val uncachedKeys = requestedKeys.subtract(cachedKeys)
val cachedJobs = cachedKeys.map { jobSpecCache[it]!! }
val fetchedJobs = jobDatabase.getJobSpecsByKeys(uncachedKeys)
val sorted = TreeSet(EligibleFullJobComparator).apply {
addAll(cachedJobs)
addAll(fetchedJobs)
}
return sorted.toList()
}
private object EligibleMinJobComparator : Comparator<MinimalJobSpec> { private object EligibleMinJobComparator : Comparator<MinimalJobSpec> {
override fun compare(o1: MinimalJobSpec, o2: MinimalJobSpec): Int { override fun compare(o1: MinimalJobSpec, o2: MinimalJobSpec): Int {
// We want to sort by priority descending, then createTime ascending // We want to sort by priority descending, then createTime ascending

View file

@ -5,6 +5,9 @@ import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.junit.Test import org.junit.Test
import org.thoughtcrime.securesms.assertIs import org.thoughtcrime.securesms.assertIs
import org.thoughtcrime.securesms.assertIsNot
import org.thoughtcrime.securesms.assertIsNotNull
import org.thoughtcrime.securesms.assertIsNull
import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec
@ -15,6 +18,11 @@ import org.thoughtcrime.securesms.testutil.TestHelpers
import java.nio.charset.Charset import java.nio.charset.Charset
class FastJobStorageTest { class FastJobStorageTest {
companion object {
val NO_PREDICATE: (MinimalJobSpec) -> Boolean = { true }
}
@Test @Test
fun `init - all stored data available`() { fun `init - all stored data available`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
@ -313,97 +321,100 @@ class FastJobStorageTest {
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - none when earlier item in queue is running`() { fun `getNextEligibleJob - none when earlier item in queue is running`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", isRunning = true), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", isRunning = true), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q"), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q"), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2)))
subject.init() subject.init()
subject.getPendingJobsWithNoDependenciesInCreatedOrder(1).size assertIs 0 subject.getNextEligibleJob(1, NO_PREDICATE) assertIs null
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - none when all jobs are running`() { fun `getNextEligibleJob - none when all jobs are running`() {
val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", isRunning = true), emptyList(), emptyList()) val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", isRunning = true), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec)))
subject.init() subject.init()
subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size assertIs 0 subject.getNextEligibleJob(10, NO_PREDICATE) assertIs null
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - none when next run time is after current time`() { fun `getNextEligibleJob - none when next run time is after current time`() {
val currentTime = 0L val currentTime = 0L
val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 0, nextBackoffInterval = 10), emptyList(), emptyList()) val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 0, nextBackoffInterval = 10), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec)))
subject.init() subject.init()
subject.getPendingJobsWithNoDependenciesInCreatedOrder(currentTime).size assertIs 0 subject.getNextEligibleJob(currentTime, NO_PREDICATE) assertIs null
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - none when dependent on another job`() { fun `getNextEligibleJob - none when dependent on another job`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), listOf(DependencySpec("2", "1", false))) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), listOf(DependencySpec("2", "1", false)))
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2)))
subject.init() subject.init()
subject.getPendingJobsWithNoDependenciesInCreatedOrder(0).size assertIs 0 subject.getNextEligibleJob(0, NO_PREDICATE) assertIs null
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - single eligible job`() { fun `getNextEligibleJob - single eligible job`() {
val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q"), emptyList(), emptyList()) val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q"), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec)))
subject.init() subject.init()
subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size assertIs 1 subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec.jobSpec
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - multiple eligible jobs`() { fun `getNextEligibleJob - multiple eligible jobs`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1"), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1"), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2)))
subject.init() subject.init()
subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size assertIs 2 subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec1.jobSpec
subject.deleteJob(fullSpec1.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec2.jobSpec
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - single eligible job in mixed list`() { fun `getNextEligibleJob - single eligible job in mixed list`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) val job = subject.getNextEligibleJob(10, NO_PREDICATE)
jobs.size assertIs 1 job.assertIsNotNull()
jobs[0].id assertIs "2" job.id assertIs fullSpec2.jobSpec.id
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - first item in queue`() { fun `getNextEligibleJob - first item in queue`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q"), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q"), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q"), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q"), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) val job = subject.getNextEligibleJob(10, NO_PREDICATE)
jobs.size assertIs 1 job.assertIsNotNull()
jobs[0].id assertIs "1" job.id assertIs fullSpec1.jobSpec.id
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - first item in queue with priority`() { fun `getNextEligibleJob - first item in queue with priority`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", createTime = 1, priority = Job.Parameters.PRIORITY_LOW), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", createTime = 1, priority = Job.Parameters.PRIORITY_LOW), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q", createTime = 2, priority = Job.Parameters.PRIORITY_HIGH), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q", createTime = 2, priority = Job.Parameters.PRIORITY_HIGH), emptyList(), emptyList())
val fullSpec3 = FullSpec(jobSpec(id = "3", factoryKey = "f3", queueKey = "q", createTime = 3, priority = Job.Parameters.PRIORITY_DEFAULT), emptyList(), emptyList()) val fullSpec3 = FullSpec(jobSpec(id = "3", factoryKey = "f3", queueKey = "q", createTime = 3, priority = Job.Parameters.PRIORITY_DEFAULT), emptyList(), emptyList())
@ -411,13 +422,13 @@ class FastJobStorageTest {
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) val job = subject.getNextEligibleJob(10, NO_PREDICATE)
jobs.size assertIs 1 job.assertIsNotNull()
jobs[0].id assertIs "2" job.id assertIs fullSpec2.jobSpec.id
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - complex priority`() { fun `getNextEligibleJob - complex priority`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q1", createTime = 1, priority = Job.Parameters.PRIORITY_LOW), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q1", createTime = 1, priority = Job.Parameters.PRIORITY_LOW), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q1", createTime = 2, priority = Job.Parameters.PRIORITY_HIGH), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q1", createTime = 2, priority = Job.Parameters.PRIORITY_HIGH), emptyList(), emptyList())
val fullSpec3 = FullSpec(jobSpec(id = "3", factoryKey = "f3", queueKey = "q2", createTime = 3, priority = Job.Parameters.PRIORITY_DEFAULT), emptyList(), emptyList()) val fullSpec3 = FullSpec(jobSpec(id = "3", factoryKey = "f3", queueKey = "q2", createTime = 3, priority = Job.Parameters.PRIORITY_DEFAULT), emptyList(), emptyList())
@ -431,18 +442,35 @@ class FastJobStorageTest {
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3, fullSpec4, fullSpec5, fullSpec6, fullSpec7, fullSpec8, fullSpec9))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3, fullSpec4, fullSpec5, fullSpec6, fullSpec7, fullSpec8, fullSpec9)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec2.jobSpec
jobs.size assertIs 6 subject.deleteJob(fullSpec2.jobSpec.id)
jobs[0].id assertIs "2"
jobs[1].id assertIs "6" subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec6.jobSpec
jobs[2].id assertIs "3" subject.deleteJob(fullSpec6.jobSpec.id)
jobs[3].id assertIs "9"
jobs[4].id assertIs "7" subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec3.jobSpec
jobs[5].id assertIs "8" subject.deleteJob(fullSpec3.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec5.jobSpec
subject.deleteJob(fullSpec5.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec9.jobSpec
subject.deleteJob(fullSpec9.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec1.jobSpec
subject.deleteJob(fullSpec1.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec4.jobSpec
subject.deleteJob(fullSpec4.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec7.jobSpec
subject.deleteJob(fullSpec7.jobSpec.id)
subject.getNextEligibleJob(10, NO_PREDICATE) assertIs fullSpec8.jobSpec
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - lastRunAttemptTime in the future runs right away`() { fun `getNextEligibleJob - lastRunAttemptTime in the future runs right away`() {
val currentTime = 10L val currentTime = 10L
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 100, nextBackoffInterval = 5), emptyList(), emptyList()) val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 100, nextBackoffInterval = 5), emptyList(), emptyList())
@ -450,63 +478,61 @@ class FastJobStorageTest {
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1))) val subject = FastJobStorage(mockDatabase(listOf(fullSpec1)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(currentTime) val job = subject.getNextEligibleJob(currentTime, NO_PREDICATE)
jobs.size assertIs 1 job.assertIsNotNull()
jobs[0].id assertIs "1" job.id assertIs fullSpec1.jobSpec.id
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - migration job takes precedence`() { fun `getNextEligibleJob - migration job takes precedence`() {
val plainSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", createTime = 0), emptyList(), emptyList()) val plainSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", createTime = 0), emptyList(), emptyList())
val migrationSpec = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5), emptyList(), emptyList()) val migrationSpec = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(plainSpec, migrationSpec))) val subject = FastJobStorage(mockDatabase(listOf(plainSpec, migrationSpec)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) val job = subject.getNextEligibleJob(10, NO_PREDICATE)
jobs.size assertIs 1 job.assertIsNotNull()
jobs[0].id assertIs "2" job.id assertIs migrationSpec.jobSpec.id
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - running migration blocks normal jobs`() { fun `getNextEligibleJob - running migration blocks normal jobs`() {
val plainSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", createTime = 0), emptyList(), emptyList()) val plainSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", createTime = 0), emptyList(), emptyList())
val migrationSpec = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5, isRunning = true), emptyList(), emptyList()) val migrationSpec = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5, isRunning = true), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(plainSpec, migrationSpec))) val subject = FastJobStorage(mockDatabase(listOf(plainSpec, migrationSpec)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) subject.getNextEligibleJob(10, NO_PREDICATE).assertIsNull()
jobs.size assertIs 0
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - running migration blocks later migration jobs`() { fun `getNextEligibleJob - running migration blocks later migration jobs`() {
val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0, isRunning = true), emptyList(), emptyList()) val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0, isRunning = true), emptyList(), emptyList())
val migrationSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5), emptyList(), emptyList()) val migrationSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2))) val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) subject.getNextEligibleJob(10, NO_PREDICATE).assertIsNull()
jobs.size assertIs 0
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - only return first eligible migration job`() { fun `getNextEligibleJob - only return first eligible migration job`() {
val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0), emptyList(), emptyList()) val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0), emptyList(), emptyList())
val migrationSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5), emptyList(), emptyList()) val migrationSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2))) val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) val job = subject.getNextEligibleJob(10, NO_PREDICATE)
jobs.size assertIs 1 job.assertIsNotNull()
jobs[0].id assertIs "1" job.id assertIs migrationSpec1.jobSpec.id
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - migration job that isn't scheduled to run yet blocks later migration jobs`() { fun `getNextEligibleJob - migration job that isn't scheduled to run yet blocks later migration jobs`() {
val currentTime = 10L val currentTime = 10L
val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0, lastRunAttemptTime = 0, nextBackoffInterval = 999), emptyList(), emptyList()) val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0, lastRunAttemptTime = 0, nextBackoffInterval = 999), emptyList(), emptyList())
@ -515,26 +541,22 @@ class FastJobStorageTest {
val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2))) val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2)))
subject.init() subject.init()
val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(currentTime) subject.getNextEligibleJob(currentTime, NO_PREDICATE).assertIsNull()
jobs.size assertIs 0
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - after deleted, no longer is in eligible list`() { fun `getNextEligibleJob - after deleted, no longer is in eligible list`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true subject.deleteJob(DataSet1.JOB_1.id)
subject.deleteJobs(listOf("id1")) subject.getNextEligibleJob(100, NO_PREDICATE) assertIsNot DataSet1.JOB_1
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100)
jobs.contains(DataSet1.JOB_1) assertIs false
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - after deleted, next item in queue is eligible`() { fun `getNextEligibleJob - after deleted, next item in queue is eligible`() {
// Two jobs in the same queue but with different create times // Two jobs in the same queue but with different create times
val firstJob = DataSet1.JOB_1 val firstJob = DataSet1.JOB_1
val secondJob = DataSet1.JOB_1.copy(id = "id2", createTime = 2) val secondJob = DataSet1.JOB_1.copy(id = "id2", createTime = 2)
@ -548,129 +570,101 @@ class FastJobStorageTest {
) )
subject.init() subject.init()
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs firstJob
jobs.size assertIs 1 subject.deleteJob(firstJob.id)
jobs.contains(firstJob) assertIs true
subject.deleteJobs(listOf("id1")) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs secondJob
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100)
jobs.size assertIs 1
jobs.contains(firstJob) assertIs false
jobs.contains(secondJob) assertIs true
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - after marked running, no longer is in eligible list`() { fun `getNextEligibleJob - after marked running, no longer is in eligible list`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true subject.markJobAsRunning(DataSet1.JOB_1.id, 1)
subject.markJobAsRunning("id1", 1) subject.getNextEligibleJob(100, NO_PREDICATE) assertIsNot DataSet1.JOB_1
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100)
jobs.contains(DataSet1.JOB_1) assertIs false
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - after updateJobAfterRetry to be invalid, no longer is in eligible list`() { fun `getNextEligibleJob - after updateJobAfterRetry to be invalid, no longer is in eligible list`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true subject.updateJobAfterRetry(DataSet1.JOB_1.id, 1, 1000, 1_000_000, null)
subject.updateJobAfterRetry("id1", 1, 1000, 1_000_000, null) subject.getNextEligibleJob(100, NO_PREDICATE) assertIsNot DataSet1.JOB_1
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100)
jobs.contains(DataSet1.JOB_1) assertIs false
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - after invalid then marked pending, is in eligible list`() { fun `getNextEligibleJob - after invalid then marked pending, is in eligible list`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
subject.markJobAsRunning("id1", 1) subject.markJobAsRunning(DataSet1.JOB_1.id, 1)
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIsNot DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs false
subject.updateAllJobsToBePending() subject.updateAllJobsToBePending()
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE)?.id assertIs DataSet1.JOB_1.id // The last run attempt time changes, so some fields will be different
jobs.filter { it.id == DataSet1.JOB_1.id }.size assertIs 1 // The last run attempt time changes, so some fields will be different
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - after updateJobs to be invalid, no longer is in eligible list`() { fun `getNextEligibleJob - after updateJobs to be invalid, no longer is in eligible list`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true
subject.updateJobs(listOf(DataSet1.JOB_1.copy(isRunning = true))) subject.updateJobs(listOf(DataSet1.JOB_1.copy(isRunning = true)))
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIsNot DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs false
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - newly-inserted higher-priority job in queue replaces old`() { fun `getNextEligibleJob - newly-inserted higher-priority job in queue replaces old`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true
val higherPriorityJob = DataSet1.JOB_1.copy(id = "id-bigboi", priority = Job.Parameters.PRIORITY_HIGH) val higherPriorityJob = DataSet1.JOB_1.copy(id = "id-bigboi", priority = Job.Parameters.PRIORITY_HIGH)
subject.insertJobs(listOf(FullSpec(jobSpec = higherPriorityJob, constraintSpecs = emptyList(), dependencySpecs = emptyList()))) subject.insertJobs(listOf(FullSpec(jobSpec = higherPriorityJob, constraintSpecs = emptyList(), dependencySpecs = emptyList())))
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs higherPriorityJob
jobs.contains(DataSet1.JOB_1) assertIs false
jobs.contains(higherPriorityJob) assertIs true
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - updating job to have a higher priority replaces lower priority in queue`() { fun `getNextEligibleJob - updating job to have a higher priority replaces lower priority in queue`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
val lowerPriorityJob = DataSet1.JOB_1.copy(id = "id-bigboi", priority = Job.Parameters.PRIORITY_LOW) val lowerPriorityJob = DataSet1.JOB_1.copy(id = "id-bigboi", priority = Job.Parameters.PRIORITY_LOW)
subject.insertJobs(listOf(FullSpec(jobSpec = lowerPriorityJob, constraintSpecs = emptyList(), dependencySpecs = emptyList()))) subject.insertJobs(listOf(FullSpec(jobSpec = lowerPriorityJob, constraintSpecs = emptyList(), dependencySpecs = emptyList())))
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true
jobs.contains(lowerPriorityJob) assertIs false
val higherPriorityJob = lowerPriorityJob.copy(priority = Job.Parameters.PRIORITY_HIGH) val higherPriorityJob = lowerPriorityJob.copy(priority = Job.Parameters.PRIORITY_HIGH)
subject.updateJobs(listOf(higherPriorityJob)) subject.updateJobs(listOf(higherPriorityJob))
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs higherPriorityJob
jobs.contains(DataSet1.JOB_1) assertIs false
jobs.contains(higherPriorityJob) assertIs true
} }
@Test @Test
fun `getPendingJobsWithNoDependenciesInCreatedOrder - updating job to have an older createTime replaces newer in queue`() { fun `getNextEligibleJob - updating job to have an older createTime replaces newer in queue`() {
val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS))
subject.init() subject.init()
val newerJob = DataSet1.JOB_1.copy(id = "id-bigboi", createTime = 1000) val newerJob = DataSet1.JOB_1.copy(id = "id-bigboi", createTime = 1000)
subject.insertJobs(listOf(FullSpec(jobSpec = newerJob, constraintSpecs = emptyList(), dependencySpecs = emptyList()))) subject.insertJobs(listOf(FullSpec(jobSpec = newerJob, constraintSpecs = emptyList(), dependencySpecs = emptyList())))
var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs DataSet1.JOB_1
jobs.contains(DataSet1.JOB_1) assertIs true
jobs.contains(newerJob) assertIs false
val olderJob = newerJob.copy(createTime = 0) val olderJob = newerJob.copy(createTime = 0)
subject.updateJobs(listOf(olderJob)) subject.updateJobs(listOf(olderJob))
jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) subject.getNextEligibleJob(100, NO_PREDICATE) assertIs olderJob
jobs.contains(DataSet1.JOB_1) assertIs false
jobs.contains(olderJob) assertIs true
} }
@Test @Test
@ -825,10 +819,6 @@ class FastJobStorageTest {
every { mock.getAllDependencySpecs() } returns dependencies every { mock.getAllDependencySpecs() } returns dependencies
every { mock.getConstraintSpecsForJobs(any()) } returns constraints every { mock.getConstraintSpecsForJobs(any()) } returns constraints
every { mock.getJobSpec(any()) } answers { jobs.first { it.id == firstArg() } } every { mock.getJobSpec(any()) } answers { jobs.first { it.id == firstArg() } }
every { mock.getJobSpecsByKeys(any()) } answers {
val ids: Collection<String> = firstArg()
jobs.filter { ids.contains(it.id) }
}
every { mock.insertJobs(any()) } answers { every { mock.insertJobs(any()) } answers {
val inserts: List<FullSpec> = firstArg() val inserts: List<FullSpec> = firstArg()
for (insert in inserts) { for (insert in inserts) {

View file

@ -7,12 +7,22 @@ package org.thoughtcrime.securesms
import org.hamcrest.MatcherAssert import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers import org.hamcrest.Matchers
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun <T : Any?> T.assertIsNull() { fun <T : Any?> T.assertIsNull() {
contract {
returns() implies (this@assertIsNull == null)
}
MatcherAssert.assertThat(this, Matchers.nullValue()) MatcherAssert.assertThat(this, Matchers.nullValue())
} }
@OptIn(ExperimentalContracts::class)
fun <T : Any?> T.assertIsNotNull() { fun <T : Any?> T.assertIsNotNull() {
contract {
returns() implies (this@assertIsNotNull != null)
}
MatcherAssert.assertThat(this, Matchers.notNullValue()) MatcherAssert.assertThat(this, Matchers.notNullValue())
} }
@ -20,7 +30,7 @@ infix fun <T : Any?> T.assertIs(expected: T) {
MatcherAssert.assertThat(this, Matchers.`is`(expected)) MatcherAssert.assertThat(this, Matchers.`is`(expected))
} }
infix fun <T : Any> T.assertIsNot(expected: T) { infix fun <T : Any?> T.assertIsNot(expected: T) {
MatcherAssert.assertThat(this, Matchers.not(Matchers.`is`(expected))) MatcherAssert.assertThat(this, Matchers.not(Matchers.`is`(expected)))
} }