From 973dc72cfa5abbd1f5db6f37e7e736a6e6dd6241 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 16 Jul 2024 10:53:10 -0400 Subject: [PATCH] Use a minimal job spec representation in memory. --- .../securesms/database/JobDatabase.kt | 56 ++++- .../securesms/jobs/FastJobStorage.kt | 192 +++++++++++----- .../securesms/jobs/MinimalJobSpec.kt | 22 ++ .../securesms/jobs/FastJobStorageTest.kt | 208 +++++++++++------- .../core/util/SQLiteDatabaseExtensions.kt | 4 + 5 files changed, 340 insertions(+), 142 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/MinimalJobSpec.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt index 1b1cb03462..924c22be83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt @@ -13,6 +13,7 @@ import org.signal.core.util.delete import org.signal.core.util.insertInto import org.signal.core.util.logging.Log import org.signal.core.util.readToList +import org.signal.core.util.readToSingleObject import org.signal.core.util.requireBlob import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt @@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec +import org.thoughtcrime.securesms.jobs.MinimalJobSpec class JobDatabase( application: Application, @@ -183,6 +185,43 @@ class JobDatabase( @Synchronized fun getAllJobSpecs(): List { + return readableDatabase + .select() + .from(Jobs.TABLE_NAME) + .orderBy("${Jobs.CREATE_TIME}, ${Jobs.ID} ASC") + .run() + .readToList { cursor -> + jobSpecFromCursor(cursor) + } + } + + @Synchronized + fun getOldestJobSpecs(limit: Int): List { + return readableDatabase + .select() + .from(Jobs.TABLE_NAME) + .orderBy("${Jobs.CREATE_TIME}, ${Jobs.ID} ASC") + .limit(limit) + .run() + .readToList { cursor -> + jobSpecFromCursor(cursor) + } + } + + @Synchronized + fun getJobSpec(id: String): JobSpec? { + return readableDatabase + .select() + .from(Jobs.TABLE_NAME) + .where("${Jobs.JOB_SPEC_ID} = ?", id) + .run() + .readToSingleObject { + jobSpecFromCursor(it) + } + } + + @Synchronized + fun getAllMinimalJobSpecs(): List { val columns = arrayOf( Jobs.ID, Jobs.JOB_SPEC_ID, @@ -191,18 +230,23 @@ class JobDatabase( Jobs.CREATE_TIME, Jobs.LAST_RUN_ATTEMPT_TIME, Jobs.NEXT_BACKOFF_INTERVAL, - Jobs.RUN_ATTEMPT, - Jobs.MAX_ATTEMPTS, - Jobs.LIFESPAN, - Jobs.SERIALIZED_DATA, - Jobs.SERIALIZED_INPUT_DATA, Jobs.IS_RUNNING, Jobs.PRIORITY ) return readableDatabase .query(Jobs.TABLE_NAME, columns, null, null, null, null, "${Jobs.CREATE_TIME}, ${Jobs.ID} ASC") .readToList { cursor -> - jobSpecFromCursor(cursor) + MinimalJobSpec( + id = cursor.requireNonNullString(Jobs.JOB_SPEC_ID), + factoryKey = cursor.requireNonNullString(Jobs.FACTORY_KEY), + queueKey = cursor.requireNonNullString(Jobs.QUEUE_KEY), + createTime = cursor.requireLong(Jobs.CREATE_TIME), + lastRunAttemptTime = cursor.requireLong(Jobs.LAST_RUN_ATTEMPT_TIME), + nextBackoffInterval = cursor.requireLong(Jobs.NEXT_BACKOFF_INTERVAL), + priority = cursor.requireInt(Jobs.PRIORITY), + isRunning = cursor.requireBoolean(Jobs.IS_RUNNING), + isMemoryOnly = false + ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.kt index 5fc0003868..e3f8e86873 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/FastJobStorage.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.jobs +import androidx.annotation.VisibleForTesting import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec @@ -7,33 +8,43 @@ import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage +import org.thoughtcrime.securesms.util.LRUCache import java.util.TreeSet class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { - // TODO [job] We need a new jobspec that has no data (for space efficiency), and ideally no other random stuff that we don't need for filtering + companion object { + private const val JOB_CACHE_LIMIT = 1000 + } - private val jobs: MutableList = mutableListOf() + private val jobSpecCache: LRUCache = LRUCache(JOB_CACHE_LIMIT) + + private val jobs: MutableList = mutableListOf() + + // TODO [job] Rather than duplicate what is likely the same handful of constraints over and over, we should somehow re-use instances private val constraintsByJobId: MutableMap> = mutableMapOf() private val dependenciesByJobId: MutableMap> = mutableMapOf() - private val eligibleJobs: TreeSet = TreeSet(EligibleJobComparator) - private val migrationJobs: TreeSet = TreeSet(compareBy { it.createTime }) - private val mostEligibleJobForQueue: MutableMap = hashMapOf() + private val eligibleJobs: TreeSet = TreeSet(EligibleJobComparator) + private val migrationJobs: TreeSet = TreeSet(compareBy { it.createTime }) + private val mostEligibleJobForQueue: MutableMap = hashMapOf() @Synchronized override fun init() { - jobs += jobDatabase.getAllJobSpecs() + jobs += jobDatabase.getAllMinimalJobSpecs() for (job in jobs) { if (job.queueKey == Job.Parameters.MIGRATION_QUEUE_KEY) { migrationJobs += job } else { - // TODO [job] Because we're using a TreeSet, this operation becomes n*log(n). Ideal complexity for a sort, but think more about whether a bulk sort would be better. placeJobInEligibleList(job) } } + jobDatabase.getOldestJobSpecs(JOB_CACHE_LIMIT).forEach { + jobSpecCache[it.id] = it + } + for (constraintSpec in jobDatabase.getAllConstraintSpecs()) { val jobConstraints: MutableList = constraintsByJobId.getOrPut(constraintSpec.jobSpecId) { mutableListOf() } jobConstraints += constraintSpec @@ -54,12 +65,14 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { } for (fullSpec in fullSpecs) { - jobs += fullSpec.jobSpec + val minimalJobSpec = fullSpec.jobSpec.toMinimalJobSpec() + jobs += minimalJobSpec + jobSpecCache[fullSpec.jobSpec.id] = fullSpec.jobSpec if (fullSpec.jobSpec.queueKey == Job.Parameters.MIGRATION_QUEUE_KEY) { - migrationJobs += fullSpec.jobSpec + migrationJobs += minimalJobSpec } else { - placeJobInEligibleList(fullSpec.jobSpec) + placeJobInEligibleList(minimalJobSpec) } constraintsByJobId[fullSpec.jobSpec.id] = fullSpec.constraintSpecs.toMutableList() @@ -69,21 +82,21 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { @Synchronized override fun getJobSpec(id: String): JobSpec? { - return jobs.firstOrNull { it.id == id } + return jobs.firstOrNull { it.id == id }?.toJobSpec() } @Synchronized override fun getAllJobSpecs(): List { // TODO [job] this will have to change - return ArrayList(jobs) + return jobDatabase.getAllJobSpecs() } @Synchronized override fun getPendingJobsWithNoDependenciesInCreatedOrder(currentTime: Long): List { - val migrationJob: JobSpec? = migrationJobs.firstOrNull() + val migrationJob: MinimalJobSpec? = migrationJobs.firstOrNull() return if (migrationJob != null && !migrationJob.isRunning && migrationJob.hasEligibleRunTime(currentTime)) { - listOf(migrationJob) + listOf(migrationJob.toJobSpec()) } else if (migrationJob != null) { emptyList() } else { @@ -95,15 +108,16 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { } .filterNot { it.isRunning } .filter { job -> job.hasEligibleRunTime(currentTime) } + .map { it.toJobSpec() } .toList() - - // Note: The priority sort at the end is safe because it's stable. That means that within jobs with the same priority, they will still be sorted by createTime. } } @Synchronized override fun getJobsInQueue(queue: String): List { - return jobs.filter { it.queueKey == queue } + return jobs + .filter { it.queueKey == queue } + .map { it.toJobSpec() } } @Synchronized @@ -130,9 +144,10 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { val job: JobSpec? = getJobSpec(id) if (job == null || !job.isMemoryOnly) { jobDatabase.markJobAsRunning(id, currentTime) + // Don't need to update jobSpecCache because all changed fields are in the min spec } - updateJobsInMemory( + updateCachedJobSpecs( filter = { it.id == id }, transformer = { jobSpec -> jobSpec.copy( @@ -149,46 +164,35 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { val job = getJobSpec(id) if (job == null || !job.isMemoryOnly) { jobDatabase.updateJobAfterRetry(id, currentTime, runAttempt, nextBackoffInterval, serializedData) + + // Note: All other fields are accounted for in the min spec. We only need to update from disk if serialized data changes. + val cached = jobSpecCache[id] + if (cached != null && !cached.serializedData.contentEquals(serializedData)) { + jobDatabase.getJobSpec(id)?.let { + jobSpecCache[id] = it + } + } } - updateJobsInMemory( + updateCachedJobSpecs( filter = { it.id == id }, transformer = { jobSpec -> jobSpec.copy( isRunning = false, - runAttempt = runAttempt, lastRunAttemptTime = currentTime, - nextBackoffInterval = nextBackoffInterval, - serializedData = serializedData + nextBackoffInterval = nextBackoffInterval ) }, singleUpdate = true ) } - private fun updateJobsInMemory(filter: (JobSpec) -> Boolean, transformer: (JobSpec) -> JobSpec, singleUpdate: Boolean = false) { - val iterator = jobs.listIterator() - - while (iterator.hasNext()) { - val current = iterator.next() - - if (filter(current)) { - val updated = transformer(current) - iterator.set(updated) - replaceJobInEligibleList(current, updated) - - if (singleUpdate) { - return - } - } - } - } - @Synchronized override fun updateAllJobsToBePending() { jobDatabase.updateAllJobsToBePending() + // Don't need to update jobSpecCache because all changed fields are in the min spec - updateJobsInMemory( + updateCachedJobSpecs( filter = { it.isRunning }, transformer = { jobSpec -> jobSpec.copy( @@ -210,12 +214,18 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { jobDatabase.updateJobs(durable) } - val updatesById: Map = jobSpecs.associateBy { it.id } + val updatesById: Map = jobSpecs + .map { it.toMinimalJobSpec() } + .associateBy { it.id } - updateJobsInMemory( + updateCachedJobSpecs( filter = { updatesById.containsKey(it.id) }, transformer = { updatesById.getValue(it.id) } ) + + for (update in jobSpecs) { + jobSpecCache[update.id] = update + } } @Synchronized @@ -228,18 +238,24 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { val jobsToDelete: Set = jobIds .mapNotNull { getJobSpec(it) } .toSet() + val durableJobIdsToDelete: List = jobsToDelete .filterNot { it.isMemoryOnly } .map { it.id } + val minimalJobsToDelete: Set = jobsToDelete + .map { it.toMinimalJobSpec() } + .toSet() + if (durableJobIdsToDelete.isNotEmpty()) { jobDatabase.deleteJobs(durableJobIdsToDelete) } val deleteIds: Set = jobIds.toSet() jobs.removeIf { deleteIds.contains(it.id) } - eligibleJobs.removeAll(jobsToDelete) - migrationJobs.removeAll(jobsToDelete) + jobSpecCache.keys.removeAll(deleteIds) + eligibleJobs.removeAll(minimalJobsToDelete) + migrationJobs.removeAll(minimalJobsToDelete) for (jobId in jobIds) { constraintsByJobId.remove(jobId) @@ -289,8 +305,44 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { return dependenciesByJobId.values.flatten() } - private fun placeJobInEligibleList(job: JobSpec) { - var jobToPlace: JobSpec? = job + private fun updateCachedJobSpecs(filter: (MinimalJobSpec) -> Boolean, transformer: (MinimalJobSpec) -> MinimalJobSpec, singleUpdate: Boolean = false) { + val iterator = jobs.listIterator() + + while (iterator.hasNext()) { + val current = iterator.next() + + if (filter(current)) { + val updated = transformer(current) + iterator.set(updated) + replaceJobInEligibleList(current, updated) + + jobSpecCache.remove(current.id)?.let { currentJobSpec -> + val updatedJobSpec = currentJobSpec.copy( + id = updated.id, + factoryKey = updated.factoryKey, + queueKey = updated.queueKey, + createTime = updated.createTime, + lastRunAttemptTime = updated.lastRunAttemptTime, + nextBackoffInterval = updated.nextBackoffInterval, + priority = updated.priority, + isRunning = updated.isRunning, + isMemoryOnly = updated.isMemoryOnly + ) + jobSpecCache[updatedJobSpec.id] = updatedJobSpec + } + + if (singleUpdate) { + return + } + } + } + } + + /** + * Heart of a lot of the in-memory job management. Will ensure that we have an up-to-date list of eligible jobs in sorted order. + */ + private fun placeJobInEligibleList(job: MinimalJobSpec) { + var jobToPlace: MinimalJobSpec? = job if (job.queueKey != null) { val existingJobInQueue = mostEligibleJobForQueue[job.queueKey] @@ -320,7 +372,10 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { eligibleJobs += jobToPlace } - private fun replaceJobInEligibleList(current: JobSpec?, updated: JobSpec?) { + /** + * Replaces a job in the eligible list with an updated version of the job. + */ + private fun replaceJobInEligibleList(current: MinimalJobSpec?, updated: MinimalJobSpec?) { if (current == null || updated == null) { return } @@ -372,7 +427,7 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { /** * Whether or not the job's eligible to be run based off of it's [Job.nextBackoffInterval] and other properties. */ - private fun JobSpec.hasEligibleRunTime(currentTime: Long): Boolean { + private fun MinimalJobSpec.hasEligibleRunTime(currentTime: Long): Boolean { return this.lastRunAttemptTime > currentTime || (this.lastRunAttemptTime + this.nextBackoffInterval) < currentTime } @@ -383,12 +438,23 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { .filter { it.dependsOnJobId == jobSpecId } } - private object EligibleJobComparator : Comparator { - override fun compare(o1: JobSpec, o2: JobSpec): Int { + /** + * Converts a [MinimalJobSpec] to a [JobSpec]. We prefer using the cache, but if it's not found, we'll hit the database. + * We consider this a "recent access" and will cache it for future use. + */ + private fun MinimalJobSpec.toJobSpec(): JobSpec { + return jobSpecCache.getOrPut(this.id) { + jobDatabase.getJobSpec(this.id) ?: throw IllegalArgumentException("JobSpec not found for id: $id") + } + } + + private object EligibleJobComparator : Comparator { + override fun compare(o1: MinimalJobSpec, o2: MinimalJobSpec): Int { // We want to sort by priority descending, then createTime ascending // CAUTION: This is used by a TreeSet, so it must be consistent with equals. // If this compare function says two objects are equal, then only one will be allowed in the set! + // This is why the last step is to compare the IDs. return when { o1.priority > o2.priority -> -1 o1.priority < o2.priority -> 1 @@ -398,14 +464,22 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage { } } } +} - private data class MinimalJobSpec( - val id: String, - val factoryKey: String, - val queueKey: String?, - val createTime: Long, - val priority: Int, - val isRunning: Boolean, - val isMemoryOnly: Boolean +/** + * Converts a [JobSpec] to a [MinimalJobSpec], which is just a matter of trimming off unnecessary properties. + */ +@VisibleForTesting +fun JobSpec.toMinimalJobSpec(): MinimalJobSpec { + return MinimalJobSpec( + id = this.id, + factoryKey = this.factoryKey, + queueKey = this.queueKey, + createTime = this.createTime, + lastRunAttemptTime = this.lastRunAttemptTime, + nextBackoffInterval = this.nextBackoffInterval, + priority = this.priority, + isRunning = this.isRunning, + isMemoryOnly = this.isMemoryOnly ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MinimalJobSpec.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MinimalJobSpec.kt new file mode 100644 index 0000000000..f95a3bb03f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MinimalJobSpec.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +/** + * A smaller version of [org.thoughtcrime.securesms.jobmanager.persistence.JobSpec] that contains on the the data we need + * to sort and pick jobs in [FastJobStorage]. + */ +data class MinimalJobSpec( + val id: String, + val factoryKey: String, + val queueKey: String?, + val createTime: Long, + val lastRunAttemptTime: Long, + val nextBackoffInterval: Long, + val priority: Int, + val isRunning: Boolean, + val isMemoryOnly: Boolean +) diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.kt index 47fad19205..bc9a904101 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.kt @@ -1,7 +1,9 @@ package org.thoughtcrime.securesms.jobs +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import org.junit.Test -import org.mockito.Mockito import org.thoughtcrime.securesms.assertIs import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.jobmanager.Job @@ -15,7 +17,7 @@ import java.nio.charset.Charset class FastJobStorageTest { @Test fun `init - all stored data available`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() DataSet1.assertJobsMatch(subject.allJobSpecs) @@ -25,7 +27,7 @@ class FastJobStorageTest { @Test fun `init - removes circular dependencies`() { - val subject = FastJobStorage(fixedDataDatabase(DataSetCircularDependency.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSetCircularDependency.FULL_SPECS)) subject.init() DataSetCircularDependency.assertJobsMatch(subject.allJobSpecs) @@ -35,27 +37,27 @@ class FastJobStorageTest { @Test fun `insertJobs - writes to database`() { - val database = noopDatabase() + val database = mockDatabase() val subject = FastJobStorage(database) subject.insertJobs(DataSet1.FULL_SPECS) - Mockito.verify(database).insertJobs(DataSet1.FULL_SPECS) + verify { database.insertJobs(DataSet1.FULL_SPECS) } } @Test fun `insertJobs - memory-only job does not write to database`() { - val database = noopDatabase() + val database = mockDatabase() val subject = FastJobStorage(database) subject.insertJobs(DataSetMemory.FULL_SPECS) - Mockito.verify(database, Mockito.times(0)).insertJobs(DataSet1.FULL_SPECS) + verify(exactly = 0) { database.insertJobs(DataSet1.FULL_SPECS) } } @Test fun `insertJobs - data can be found`() { - val subject = FastJobStorage(noopDatabase()) + val subject = FastJobStorage(mockDatabase()) subject.insertJobs(DataSet1.FULL_SPECS) DataSet1.assertJobsMatch(subject.allJobSpecs) DataSet1.assertConstraintsMatch(subject.allConstraintSpecs) @@ -64,7 +66,7 @@ class FastJobStorageTest { @Test fun `insertJobs - individual job can be found`() { - val subject = FastJobStorage(noopDatabase()) + val subject = FastJobStorage(mockDatabase()) subject.insertJobs(DataSet1.FULL_SPECS) subject.getJobSpec(DataSet1.JOB_1.id) assertIs DataSet1.JOB_1 @@ -73,10 +75,10 @@ class FastJobStorageTest { @Test fun `updateAllJobsToBePending - writes to database`() { - val database = noopDatabase() + val database = mockDatabase() val subject = FastJobStorage(database) subject.updateAllJobsToBePending() - Mockito.verify(database).updateAllJobsToBePending() + verify { database.updateAllJobsToBePending() } } @Test @@ -84,7 +86,7 @@ class FastJobStorageTest { val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", isRunning = true), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) subject.init() subject.updateAllJobsToBePending() @@ -94,26 +96,26 @@ class FastJobStorageTest { @Test fun `updateJobs - writes to database`() { - val database = fixedDataDatabase(DataSet1.FULL_SPECS) + val database = mockDatabase(DataSet1.FULL_SPECS) val jobs = listOf(jobSpec(id = "id1", factoryKey = "f1")) val subject = FastJobStorage(database) subject.init() subject.updateJobs(jobs) - Mockito.verify(database).updateJobs(jobs) + verify { database.updateJobs(jobs) } } @Test fun `updateJobs - memory-only job does not write to database`() { - val database = fixedDataDatabase(DataSetMemory.FULL_SPECS) + val database = mockDatabase(DataSetMemory.FULL_SPECS) val jobs = listOf(jobSpec(id = "id1", factoryKey = "f1")) val subject = FastJobStorage(database) subject.init() subject.updateJobs(jobs) - Mockito.verify(database, Mockito.times(0)).updateJobs(jobs) + verify(exactly = 0) { database.updateJobs(jobs) } } @Test @@ -153,7 +155,7 @@ class FastJobStorageTest { isMemoryOnly = false ) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2, fullSpec3))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3))) subject.init() subject.updateJobs(listOf(update1, update2)) @@ -164,19 +166,19 @@ class FastJobStorageTest { @Test fun `markJobAsRunning - writes to database`() { - val database = fixedDataDatabase(DataSet1.FULL_SPECS) + val database = mockDatabase(DataSet1.FULL_SPECS) val subject = FastJobStorage(database) subject.init() subject.markJobAsRunning(id = "id1", currentTime = 42) - Mockito.verify(database).markJobAsRunning(id = "id1", currentTime = 42) + verify { database.markJobAsRunning(id = "id1", currentTime = 42) } } @Test fun `markJobAsRunning - state updated`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.markJobAsRunning(id = DataSet1.JOB_1.id, currentTime = 42) @@ -187,7 +189,7 @@ class FastJobStorageTest { @Test fun `updateJobAfterRetry - writes to database`() { - val database = fixedDataDatabase(DataSet1.FULL_SPECS) + val database = mockDatabase(DataSet1.FULL_SPECS) val subject = FastJobStorage(database) subject.init() @@ -200,12 +202,12 @@ class FastJobStorageTest { serializedData = "a".toByteArray() ) - Mockito.verify(database).updateJobAfterRetry(id = "id1", currentTime = 0, runAttempt = 1, nextBackoffInterval = 10, serializedData = "a".toByteArray()) + verify { database.updateJobAfterRetry(id = "id1", currentTime = 0, runAttempt = 1, nextBackoffInterval = 10, serializedData = "a".toByteArray()) } } @Test fun `updateJobAfterRetry - memory-only job does not write to database`() { - val database = fixedDataDatabase(DataSetMemory.FULL_SPECS) + val database = mockDatabase(DataSetMemory.FULL_SPECS) val subject = FastJobStorage(database) subject.init() @@ -218,14 +220,14 @@ class FastJobStorageTest { serializedData = "a".toByteArray() ) - Mockito.verify(database, Mockito.times(0)).updateJobAfterRetry(id = "id1", currentTime = 0, runAttempt = 1, nextBackoffInterval = 10, serializedData = "a".toByteArray()) + verify(exactly = 0) { database.updateJobAfterRetry(id = "id1", currentTime = 0, runAttempt = 1, nextBackoffInterval = 10, serializedData = "a".toByteArray()) } } @Test fun `updateJobAfterRetry - state updated`() { val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) subject.init() subject.updateJobAfterRetry( @@ -250,7 +252,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) subject.init() subject.getPendingJobsWithNoDependenciesInCreatedOrder(1).size assertIs 0 @@ -260,7 +262,7 @@ class FastJobStorageTest { fun `getPendingJobsWithNoDependenciesInCreatedOrder - none when all jobs are running`() { val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", isRunning = true), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) subject.init() subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size assertIs 0 @@ -271,7 +273,7 @@ class FastJobStorageTest { val currentTime = 0L val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 0, nextBackoffInterval = 10), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) subject.init() subject.getPendingJobsWithNoDependenciesInCreatedOrder(currentTime).size assertIs 0 @@ -282,7 +284,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) subject.init() subject.getPendingJobsWithNoDependenciesInCreatedOrder(0).size assertIs 0 @@ -292,7 +294,7 @@ class FastJobStorageTest { fun `getPendingJobsWithNoDependenciesInCreatedOrder - single eligible job`() { val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q"), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec))) subject.init() subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size assertIs 1 @@ -303,7 +305,7 @@ class FastJobStorageTest { val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1"), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) subject.init() subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size assertIs 2 @@ -314,7 +316,7 @@ class FastJobStorageTest { val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", isRunning = true), emptyList(), emptyList()) val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2"), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -327,7 +329,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -341,7 +343,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1, fullSpec2, fullSpec3))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -361,7 +363,7 @@ class FastJobStorageTest { val fullSpec8 = FullSpec(jobSpec(id = "8", factoryKey = "f8", queueKey = null, createTime = 8, priority = Job.Parameters.PRIORITY_LOW), emptyList(), emptyList()) val fullSpec9 = FullSpec(jobSpec(id = "9", factoryKey = "f9", queueKey = null, createTime = 9, priority = Job.Parameters.PRIORITY_DEFAULT), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(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() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -380,7 +382,7 @@ class FastJobStorageTest { val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 100, nextBackoffInterval = 5), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(fullSpec1))) + val subject = FastJobStorage(mockDatabase(listOf(fullSpec1))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(currentTime) @@ -393,7 +395,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(plainSpec, migrationSpec))) + val subject = FastJobStorage(mockDatabase(listOf(plainSpec, migrationSpec))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -406,7 +408,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(plainSpec, migrationSpec))) + val subject = FastJobStorage(mockDatabase(listOf(plainSpec, migrationSpec))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -418,7 +420,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(migrationSpec1, migrationSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -430,7 +432,7 @@ class FastJobStorageTest { 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 subject = FastJobStorage(fixedDataDatabase(listOf(migrationSpec1, migrationSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10) @@ -445,7 +447,7 @@ class FastJobStorageTest { val migrationSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 0, lastRunAttemptTime = 0, nextBackoffInterval = 999), emptyList(), emptyList()) val migrationSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = Job.Parameters.MIGRATION_QUEUE_KEY, createTime = 5, lastRunAttemptTime = 0, nextBackoffInterval = 0), emptyList(), emptyList()) - val subject = FastJobStorage(fixedDataDatabase(listOf(migrationSpec1, migrationSpec2))) + val subject = FastJobStorage(mockDatabase(listOf(migrationSpec1, migrationSpec2))) subject.init() val jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(currentTime) @@ -454,7 +456,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - after deleted, no longer is in eligible list`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) @@ -468,7 +470,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - after marked running, no longer is in eligible list`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) @@ -482,7 +484,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - after updateJobAfterRetry to be invalid, no longer is in eligible list`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) @@ -496,7 +498,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - after invalid then marked pending, is in eligible list`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.markJobAsRunning("id1", 1) @@ -511,7 +513,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - after updateJobs to be invalid, no longer is in eligible list`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) @@ -525,7 +527,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - newly-inserted higher-priority job in queue replaces old`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() var jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(100) @@ -541,7 +543,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - updating job to have a higher priority replaces lower priority in queue`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val lowerPriorityJob = DataSet1.JOB_1.copy(id = "id-bigboi", priority = Job.Parameters.PRIORITY_LOW) @@ -561,7 +563,7 @@ class FastJobStorageTest { @Test fun `getPendingJobsWithNoDependenciesInCreatedOrder - updating job to have an older createTime replaces newer in queue`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val newerJob = DataSet1.JOB_1.copy(id = "id-bigboi", createTime = 1000) @@ -581,7 +583,7 @@ class FastJobStorageTest { @Test fun `deleteJobs - writes to database`() { - val database = fixedDataDatabase(DataSet1.FULL_SPECS) + val database = mockDatabase(DataSet1.FULL_SPECS) val ids: List = listOf("id1", "id2") val subject = FastJobStorage(database) @@ -589,12 +591,12 @@ class FastJobStorageTest { subject.deleteJobs(ids) - Mockito.verify(database).deleteJobs(ids) + verify { database.deleteJobs(ids) } } @Test fun `deleteJobs - memory-only job does not write to database`() { - val database = fixedDataDatabase(DataSetMemory.FULL_SPECS) + val database = mockDatabase(DataSetMemory.FULL_SPECS) val ids = listOf("id1") val subject = FastJobStorage(database) @@ -602,12 +604,12 @@ class FastJobStorageTest { subject.deleteJobs(ids) - Mockito.verify(database, Mockito.times(0)).deleteJobs(ids) + verify(exactly = 0) { database.deleteJobs(ids) } } @Test fun `deleteJobs - deletes all relevant pieces`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.deleteJobs(listOf("id1")) @@ -622,11 +624,12 @@ class FastJobStorageTest { constraints.size assertIs 1 constraints[0] assertIs DataSet1.CONSTRAINT_2 dependencies.size assertIs 1 + subject.getJobSpec("id1") assertIs null } @Test fun `getDependencySpecsThatDependOnJob - start of chain`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val result = subject.getDependencySpecsThatDependOnJob("id1") @@ -637,7 +640,7 @@ class FastJobStorageTest { @Test fun `getDependencySpecsThatDependOnJob - mid-chain`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val result = subject.getDependencySpecsThatDependOnJob("id2") @@ -647,7 +650,7 @@ class FastJobStorageTest { @Test fun `getDependencySpecsThatDependOnJob - end of chain`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val result = subject.getDependencySpecsThatDependOnJob("id3") @@ -656,7 +659,7 @@ class FastJobStorageTest { @Test fun `getJobsInQueue - empty`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val result = subject.getJobsInQueue("x") @@ -665,7 +668,7 @@ class FastJobStorageTest { @Test fun `getJobsInQueue - single job`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() val result = subject.getJobsInQueue("q1") @@ -675,7 +678,7 @@ class FastJobStorageTest { @Test fun `getJobCountForFactory - general`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.getJobCountForFactory("f1") assertIs 1 @@ -684,7 +687,7 @@ class FastJobStorageTest { @Test fun `getJobCountForFactoryAndQueue - general`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.getJobCountForFactoryAndQueue("f1", "q1") assertIs 1 @@ -694,7 +697,7 @@ class FastJobStorageTest { @Test fun `areQueuesEmpty - all non-empty`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.areQueuesEmpty(TestHelpers.setOf("q1")) assertIs false @@ -703,7 +706,7 @@ class FastJobStorageTest { @Test fun `areQueuesEmpty - mixed empty`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.areQueuesEmpty(TestHelpers.setOf("q1", "q5")) assertIs false @@ -711,27 +714,78 @@ class FastJobStorageTest { @Test fun `areQueuesEmpty - queue does not exist`() { - val subject = FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS)) + val subject = FastJobStorage(mockDatabase(DataSet1.FULL_SPECS)) subject.init() subject.areQueuesEmpty(TestHelpers.setOf("q4")) assertIs true subject.areQueuesEmpty(TestHelpers.setOf("q4", "q5")) assertIs true } - private fun noopDatabase(): JobDatabase { - val database = Mockito.mock(JobDatabase::class.java) - Mockito.`when`(database.getAllJobSpecs()).thenReturn(emptyList()) - Mockito.`when`(database.getAllConstraintSpecs()).thenReturn(emptyList()) - Mockito.`when`(database.getAllDependencySpecs()).thenReturn(emptyList()) - return database - } + private fun mockDatabase(fullSpecs: List = emptyList()): JobDatabase { + val jobs = fullSpecs.map { it.jobSpec }.toMutableList() + val constraints = fullSpecs.map { it.constraintSpecs }.flatten().toMutableList() + val dependencies = fullSpecs.map { it.dependencySpecs }.flatten().toMutableList() - private fun fixedDataDatabase(fullSpecs: List): JobDatabase { - val database = Mockito.mock(JobDatabase::class.java) - Mockito.`when`(database.getAllJobSpecs()).thenReturn(fullSpecs.map { it.jobSpec }) - Mockito.`when`(database.getAllConstraintSpecs()).thenReturn(fullSpecs.map { it.constraintSpecs }.flatten()) - Mockito.`when`(database.getAllDependencySpecs()).thenReturn(fullSpecs.map { it.dependencySpecs }.flatten()) - return database + val mock = mockk(relaxed = true) + every { mock.getAllJobSpecs() } returns jobs + every { mock.getAllMinimalJobSpecs() } returns jobs.map { it.toMinimalJobSpec() } + every { mock.getOldestJobSpecs(any()) } answers { jobs.sortedBy { it.createTime }.take(firstArg()) } + every { mock.getAllConstraintSpecs() } returns constraints + every { mock.getAllDependencySpecs() } returns dependencies + every { mock.getJobSpec(any()) } answers { jobs.first { it.id == firstArg() } } + every { mock.insertJobs(any()) } answers { + val inserts: List = firstArg() + for (insert in inserts) { + jobs += insert.jobSpec + constraints += insert.constraintSpecs + dependencies += insert.dependencySpecs + } + } + every { mock.deleteJobs(any()) } answers { + val ids: List = firstArg() + jobs.removeIf { ids.contains(it.id) } + constraints.removeIf { ids.contains(it.jobSpecId) } + dependencies.removeIf { ids.contains(it.jobId) || ids.contains(it.dependsOnJobId) } + } + every { mock.updateJobs(any()) } answers { + val updates: List = firstArg() + for (update in updates) { + jobs.removeIf { it.id == update.id } + jobs += update + } + } + every { mock.updateAllJobsToBePending() } answers { + val iterator = jobs.listIterator() + while (iterator.hasNext()) { + val job = iterator.next() + iterator.set(job.copy(isRunning = false)) + } + } + every { mock.updateJobAfterRetry(any(), any(), any(), any(), any()) } answers { + val id = args[0] as String + val currentTime = args[1] as Long + val runAttempt = args[2] as Int + val nextBackoffInterval = args[3] as Long + val serializedData = args[4] as ByteArray? + + val iterator = jobs.listIterator() + while (iterator.hasNext()) { + val job = iterator.next() + if (job.id == id) { + iterator.set( + job.copy( + isRunning = false, + runAttempt = runAttempt, + lastRunAttemptTime = currentTime, + nextBackoffInterval = nextBackoffInterval, + serializedData = serializedData + ) + ) + } + } + } + + return mock } private fun jobSpec( diff --git a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt index 0e866ef77e..78b61396f4 100644 --- a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt @@ -202,6 +202,10 @@ class SelectBuilderPart2( return SelectBuilderPart3(db, columns, tableName, where, whereArgs) } + fun orderBy(orderBy: String): SelectBuilderPart4a { + return SelectBuilderPart4a(db, columns, tableName, "", arrayOf(), orderBy) + } + fun run(): Cursor { return db.query( SupportSQLiteQueryBuilder