diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/StoredFileAccess.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/StoredFileAccess.kt index 0cb29fbe18..e14c6f9c67 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/StoredFileAccess.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/StoredFileAccess.kt @@ -22,7 +22,7 @@ import com.namehillsoftware.handoff.promises.Promise class StoredFileAccess(private val context: Context) : AccessStoredFiles { override fun promiseStoredFile(storedFileId: Int): Promise = - promiseTableMessage { + promiseTableMessage { RepositoryAccessHelper(context).use { repositoryAccessHelper -> getStoredFile(repositoryAccessHelper, storedFileId) } @@ -32,7 +32,7 @@ class StoredFileAccess(private val context: Context) : AccessStoredFiles { getStoredFileTask(libraryId, serviceFile) override fun promiseAllStoredFiles(libraryId: LibraryId): Promise> = - promiseTableMessage> { + promiseTableMessage { RepositoryAccessHelper(context).use { repositoryAccessHelper -> repositoryAccessHelper.beginNonExclusiveTransaction().use { repositoryAccessHelper @@ -44,7 +44,7 @@ class StoredFileAccess(private val context: Context) : AccessStoredFiles { } override fun promiseDanglingFiles(): Promise> = - promiseTableMessage> { + promiseTableMessage { RepositoryAccessHelper(context).use { helper -> helper .mapSql( @@ -58,19 +58,19 @@ class StoredFileAccess(private val context: Context) : AccessStoredFiles { } override fun promiseNewStoredFile(libraryId: LibraryId, serviceFile: ServiceFile): Promise = - promiseTableMessage { + promiseTableMessage { RepositoryAccessHelper(context).use { it.createStoredFile(libraryId, serviceFile) } } override fun promiseUpdatedStoredFile(storedFile: StoredFile): Promise = - promiseTableMessage { + promiseTableMessage { RepositoryAccessHelper(context).use { it.update(tableName, storedFile) } } private fun getStoredFileTask(libraryId: LibraryId, serviceFile: ServiceFile): Promise = - promiseTableMessage { + promiseTableMessage { RepositoryAccessHelper(context).use { repositoryAccessHelper -> repositoryAccessHelper.getStoredFile( libraryId, @@ -80,7 +80,7 @@ class StoredFileAccess(private val context: Context) : AccessStoredFiles { } override fun promiseDownloadingFiles(): Promise> = - promiseTableMessage> { + promiseTableMessage { RepositoryAccessHelper(context).use { repositoryAccessHelper -> repositoryAccessHelper .mapSql( @@ -111,7 +111,7 @@ class StoredFileAccess(private val context: Context) : AccessStoredFiles { insert(tableName, StoredFile(libraryId, serviceFile, null, true)) override fun deleteStoredFile(storedFile: StoredFile): Promise = - promiseTableMessage { + promiseTableMessage { RepositoryAccessHelper(context).use { repositoryAccessHelper -> try { repositoryAccessHelper.beginTransaction().use { closeableTransaction -> diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/RetryingStoredFileUpdate.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/RetryingStoredFileUpdate.kt new file mode 100644 index 0000000000..402e64dc38 --- /dev/null +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/RetryingStoredFileUpdate.kt @@ -0,0 +1,37 @@ +package com.lasthopesoftware.bluewater.client.stored.library.items.files.updates + +import android.database.sqlite.SQLiteConstraintException +import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile +import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId +import com.lasthopesoftware.bluewater.client.stored.library.items.files.repository.StoredFile +import com.lasthopesoftware.bluewater.shared.lazyLogger +import com.lasthopesoftware.policies.retries.RecursivePromiseRetryHandler +import com.namehillsoftware.handoff.promises.Promise +import java.util.concurrent.atomic.AtomicInteger + +class RetryingStoredFileUpdate( + private val inner: UpdateStoredFiles +) : UpdateStoredFiles by inner { + + companion object { + private val logger by lazyLogger() + } + + override fun promiseStoredFileUpdate(libraryId: LibraryId, serviceFile: ServiceFile): Promise { + val attempts = AtomicInteger() + return RecursivePromiseRetryHandler.retryOnException { error -> + val attempt = attempts.incrementAndGet() + if (attempt <= 2 && (error == null || error is SQLiteConstraintException && (error.message?.startsWith("UNIQUE constraint failed")) ?: false)) { + if (attempt > 1) { + logger.warn( + "Failed to update stored file (libraryId=$libraryId, serviceFile=$serviceFile) due to unique constraint failure, retrying.", + error + ) + } + inner.promiseStoredFileUpdate(libraryId, serviceFile) + } else { + Promise(error) + } + } + } +} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/StoredFileUpdater.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/StoredFileUpdater.kt index 82cb090c74..c6cda9c364 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/StoredFileUpdater.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/StoredFileUpdater.kt @@ -47,7 +47,8 @@ class StoredFileUpdater( .keepPromise(storedFile) val promisedLibrary = libraryProvider.promiseLibrary(libraryId) - return storedFileAccess.promiseStoredFile(libraryId, serviceFile) + return storedFileAccess + .promiseStoredFile(libraryId, serviceFile) .eventually { storedFile -> storedFile ?.toPromise() diff --git a/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/GivenATypicalLibrary/WithoutTheStoredFile/AndTheUniqueConstraintFailsOnce/WhenUpdatingTheFile.kt b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/GivenATypicalLibrary/WithoutTheStoredFile/AndTheUniqueConstraintFailsOnce/WhenUpdatingTheFile.kt new file mode 100644 index 0000000000..df703c39ba --- /dev/null +++ b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/GivenATypicalLibrary/WithoutTheStoredFile/AndTheUniqueConstraintFailsOnce/WhenUpdatingTheFile.kt @@ -0,0 +1,47 @@ +package com.lasthopesoftware.bluewater.client.stored.library.items.files.updates.GivenATypicalLibrary.WithoutTheStoredFile.AndTheUniqueConstraintFailsOnce + +import android.database.sqlite.SQLiteConstraintException +import com.lasthopesoftware.AndroidContext +import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile +import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId +import com.lasthopesoftware.bluewater.client.stored.library.items.files.repository.StoredFile +import com.lasthopesoftware.bluewater.client.stored.library.items.files.updates.RetryingStoredFileUpdate +import com.lasthopesoftware.bluewater.shared.promises.extensions.toExpiringFuture +import com.lasthopesoftware.promises.extensions.toPromise +import com.namehillsoftware.handoff.promises.Promise +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.AssertionsForClassTypes.assertThat +import org.junit.Test +import java.util.concurrent.TimeUnit + +class WhenUpdatingTheFile : AndroidContext() { + + companion object { + private const val libraryId = 400 + private const val serviceFileId = "dY10xa2sD" + + private val sut by lazy { + RetryingStoredFileUpdate( + mockk { + every { promiseStoredFileUpdate(LibraryId(libraryId), ServiceFile(serviceFileId)) } returns + Promise(SQLiteConstraintException("UNIQUE constraint failed: StoredFiles.libraryId, StoredFiles.serviceId (code 2067 SQLITE_CONSTRAINT_UNIQUE)")) andThen + StoredFile().toPromise() + }, + ) + } + private var storedFile: StoredFile? = null + } + + override fun before() { + storedFile = sut + .promiseStoredFileUpdate(LibraryId(libraryId), ServiceFile(serviceFileId)) + .toExpiringFuture() + .get(1, TimeUnit.MINUTES) + } + + @Test + fun `then the file is downloaded`() { + assertThat(storedFile).isNotNull + } +} diff --git a/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/GivenATypicalLibrary/WithoutTheStoredFile/AndTheUniqueConstraintFailsTwice/WhenUpdatingTheFile.kt b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/GivenATypicalLibrary/WithoutTheStoredFile/AndTheUniqueConstraintFailsTwice/WhenUpdatingTheFile.kt new file mode 100644 index 0000000000..641ecaac11 --- /dev/null +++ b/projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/updates/GivenATypicalLibrary/WithoutTheStoredFile/AndTheUniqueConstraintFailsTwice/WhenUpdatingTheFile.kt @@ -0,0 +1,61 @@ +package com.lasthopesoftware.bluewater.client.stored.library.items.files.updates.GivenATypicalLibrary.WithoutTheStoredFile.AndTheUniqueConstraintFailsTwice + +import android.database.sqlite.SQLiteConstraintException +import com.lasthopesoftware.AndroidContext +import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile +import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId +import com.lasthopesoftware.bluewater.client.stored.library.items.files.repository.StoredFile +import com.lasthopesoftware.bluewater.client.stored.library.items.files.updates.RetryingStoredFileUpdate +import com.lasthopesoftware.bluewater.shared.promises.extensions.toExpiringFuture +import com.lasthopesoftware.promises.extensions.toPromise +import com.namehillsoftware.handoff.promises.Promise +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.AssertionsForClassTypes.assertThat +import org.junit.Test +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +class WhenUpdatingTheFile : AndroidContext() { + + companion object { + private const val libraryId = 809 + private const val serviceFileId = "NJpjEijZ5Qz" + + private val sut by lazy { + + RetryingStoredFileUpdate( + mockk { + every { promiseStoredFileUpdate(LibraryId(libraryId), ServiceFile(serviceFileId)) } returns + Promise(SQLiteConstraintException("UNIQUE constraint failed: StoredFiles.libraryId, StoredFiles.serviceId (code 2067 SQLITE_CONSTRAINT_UNIQUE)")) andThen + Promise(SQLiteConstraintException("UNIQUE constraint failed: StoredFiles.libraryId, StoredFiles.serviceId (code 2067 SQLITE_CONSTRAINT_UNIQUE)")) andThen + StoredFile().toPromise() + }, + ) + } + + private var exception: SQLiteConstraintException? = null + private var storedFile: StoredFile? = null + } + + override fun before() { + try { + storedFile = sut + .promiseStoredFileUpdate(LibraryId(libraryId), ServiceFile(serviceFileId)) + .toExpiringFuture() + .get(1, TimeUnit.MINUTES) + } catch (e: ExecutionException) { + exception = e.cause as? SQLiteConstraintException + } + } + + @Test + fun `then the file is NOT downloaded`() { + assertThat(storedFile).isNull() + } + + @Test + fun `then the exception is correct`() { + assertThat(exception?.message).isEqualTo("UNIQUE constraint failed: StoredFiles.libraryId, StoredFiles.serviceId (code 2067 SQLITE_CONSTRAINT_UNIQUE)") + } +}