diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt index 6504097..58f5b9f 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt @@ -26,7 +26,7 @@ internal abstract class AdRepoBaseV2Test { prepareCtx(MkplAdStub.prepareResult { id = MkplAdId(uuidNew) ownerId = MkplUserId.NONE - lock = MkplAdLock.NONE + lock = MkplAdLock(uuidNew) }) .toTransportCreate() ) diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoDeleteTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoDeleteTest.kt index 2e1a0f0..8db1a15 100644 --- a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoDeleteTest.kt +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoDeleteTest.kt @@ -24,6 +24,7 @@ class BizRepoDeleteTest { ownerId = userId, adType = MkplDealSide.DEMAND, visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), ) private val repo = AdRepositoryMock( invokeReadAd = { @@ -50,7 +51,7 @@ class BizRepoDeleteTest { fun repoDeleteSuccessTest() = runTest { val adToUpdate = MkplAd( id = MkplAdId("123"), - lock = MkplAdLock("123"), + lock = MkplAdLock("123-234-abc-ABC"), ) val ctx = MkplContext( command = command, diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoUpdateTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoUpdateTest.kt index 73e89fe..3834e76 100644 --- a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoUpdateTest.kt +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/BizRepoUpdateTest.kt @@ -52,7 +52,7 @@ class BizRepoUpdateTest { description = "xyz", adType = MkplDealSide.DEMAND, visibility = MkplVisibility.VISIBLE_TO_GROUP, - lock = MkplAdLock("123"), + lock = MkplAdLock("123-234-abc-ABC"), ) val ctx = MkplContext( command = command, diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/RepoByIdTests.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/RepoByIdTests.kt index 729a419..046230f 100644 --- a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/RepoByIdTests.kt +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/repo/RepoByIdTests.kt @@ -41,7 +41,7 @@ fun repoNotFoundTest(command: MkplCommand) = runTest { description = "xyz", adType = MkplDealSide.DEMAND, visibility = MkplVisibility.VISIBLE_TO_GROUP, - lock = MkplAdLock("123"), + lock = MkplAdLock("123-234-abc-ABC"), ), ) processor.exec(ctx) diff --git a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbAdIdRequest.kt b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbAdIdRequest.kt index b5836cf..2594cd9 100644 --- a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbAdIdRequest.kt +++ b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbAdIdRequest.kt @@ -2,9 +2,11 @@ package ru.otus.otuskotlin.marketplace.common.repo import ru.otus.otuskotlin.marketplace.common.models.MkplAd import ru.otus.otuskotlin.marketplace.common.models.MkplAdId +import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock data class DbAdIdRequest( val id: MkplAdId, + val lock: MkplAdLock = MkplAdLock.NONE, ) { - constructor(ad: MkplAd): this(ad.id) + constructor(ad: MkplAd): this(ad.id, ad.lock) } diff --git a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbErrors.kt b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbErrors.kt index 1d1dfb0..7f7e5ff 100644 --- a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbErrors.kt +++ b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/repo/DbErrors.kt @@ -1,7 +1,12 @@ package ru.otus.otuskotlin.marketplace.common.repo +import ru.otus.otuskotlin.marketplace.common.helpers.errorSystem +import ru.otus.otuskotlin.marketplace.common.models.MkplAd import ru.otus.otuskotlin.marketplace.common.models.MkplAdId +import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock import ru.otus.otuskotlin.marketplace.common.models.MkplError +import ru.otus.otuskotlin.marketplace.common.repo.exceptions.RepoConcurrencyException +import ru.otus.otuskotlin.marketplace.common.repo.exceptions.RepoException const val ERROR_GROUP_REPO = "repo" @@ -22,3 +27,38 @@ val errorEmptyId = DbAdResponseErr( message = "Id must not be null or blank" ) ) + +fun errorRepoConcurrency( + oldAd: MkplAd, + expectedLock: MkplAdLock, + exception: Exception = RepoConcurrencyException( + id = oldAd.id, + expectedLock = expectedLock, + actualLock = oldAd.lock, + ), +) = DbAdResponseErrWithData( + ad = oldAd, + err = MkplError( + code = "$ERROR_GROUP_REPO-concurrency", + group = ERROR_GROUP_REPO, + field = "lock", + message = "The object with ID ${oldAd.id.asString()} has been changed concurrently by another user or process", + exception = exception, + ) +) + +fun errorEmptyLock(id: MkplAdId) = DbAdResponseErr( + MkplError( + code = "$ERROR_GROUP_REPO-lock-empty", + group = ERROR_GROUP_REPO, + field = "lock", + message = "Lock for Ad ${id.asString()} is empty that is not admitted" + ) +) + +fun errorDb(e: RepoException) = DbAdResponseErr( + errorSystem( + violationCode = "dbLockEmpty", + e = e + ) +) diff --git a/ok-marketplace-be/ok-marketplace-repo-inmemory/src/commonMain/kotlin/AdRepoInMemory.kt b/ok-marketplace-be/ok-marketplace-repo-inmemory/src/commonMain/kotlin/AdRepoInMemory.kt index 55cee52..a4cfa8e 100644 --- a/ok-marketplace-be/ok-marketplace-repo-inmemory/src/commonMain/kotlin/AdRepoInMemory.kt +++ b/ok-marketplace-be/ok-marketplace-repo-inmemory/src/commonMain/kotlin/AdRepoInMemory.kt @@ -52,6 +52,7 @@ class AdRepoInMemory( val rqAd = rq.ad val id = rqAd.id.takeIf { it != MkplAdId.NONE } ?: return@tryAdMethod errorEmptyId val key = id.asString() + val oldLock = rqAd.lock.takeIf { it != MkplAdLock.NONE } ?: return@tryAdMethod errorEmptyLock(id) mutex.withLock { val oldAd = cache.get(key)?.toInternal() @@ -71,6 +72,7 @@ class AdRepoInMemory( override suspend fun deleteAd(rq: DbAdIdRequest): IDbAdResponse = tryAdMethod { val id = rq.id.takeIf { it != MkplAdId.NONE } ?: return@tryAdMethod errorEmptyId val key = id.asString() + val oldLock = rq.lock.takeIf { it != MkplAdLock.NONE } ?: return@tryAdMethod errorEmptyLock(id) mutex.withLock { val oldAd = cache.get(key)?.toInternal() diff --git a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/BaseInitAds.kt b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/BaseInitAds.kt index 12123c7..fd50d40 100644 --- a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/BaseInitAds.kt +++ b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/BaseInitAds.kt @@ -3,10 +3,14 @@ package ru.otus.otuskotlin.marketplace.backend.repo.tests import ru.otus.otuskotlin.marketplace.common.models.* abstract class BaseInitAds(private val op: String): IInitObjects { + open val lockOld: MkplAdLock = MkplAdLock("20000000-0000-0000-0000-000000000001") + open val lockBad: MkplAdLock = MkplAdLock("20000000-0000-0000-0000-000000000009") + fun createInitTestModel( suf: String, ownerId: MkplUserId = MkplUserId("owner-123"), adType: MkplDealSide = MkplDealSide.DEMAND, + lock: MkplAdLock = lockOld, ) = MkplAd( id = MkplAdId("ad-repo-$op-$suf"), title = "$suf stub", @@ -14,5 +18,6 @@ abstract class BaseInitAds(private val op: String): IInitObjects { ownerId = ownerId, visibility = MkplVisibility.VISIBLE_TO_OWNER, adType = adType, + lock = lock, ) } diff --git a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdDeleteTest.kt b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdDeleteTest.kt index a602070..99c42b7 100644 --- a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdDeleteTest.kt +++ b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdDeleteTest.kt @@ -11,11 +11,13 @@ import kotlin.test.assertNotNull abstract class RepoAdDeleteTest { abstract val repo: IRepoAd protected open val deleteSucc = initObjects[0] + protected open val deleteConc = initObjects[1] protected open val notFoundId = MkplAdId("ad-repo-delete-notFound") @Test fun deleteSuccess() = runRepoTest { - val result = repo.deleteAd(DbAdIdRequest(deleteSucc.id)) + val lockOld = deleteSucc.lock + val result = repo.deleteAd(DbAdIdRequest(deleteSucc.id, lock = lockOld)) assertIs(result) assertEquals(deleteSucc.title, result.data.title) assertEquals(deleteSucc.description, result.data.description) @@ -23,16 +25,26 @@ abstract class RepoAdDeleteTest { @Test fun deleteNotFound() = runRepoTest { - val result = repo.readAd(DbAdIdRequest(notFoundId)) + val result = repo.readAd(DbAdIdRequest(notFoundId, lock = lockOld)) assertIs(result) val error = result.errors.find { it.code == "repo-not-found" } assertNotNull(error) } + @Test + fun deleteConcurrency() = runRepoTest { + val result = repo.deleteAd(DbAdIdRequest(deleteConc.id, lock = lockBad)) + + assertIs(result) + val error = result.errors.find { it.code == "repo-concurrency" } + assertNotNull(error) + } + companion object : BaseInitAds("delete") { override val initObjects: List = listOf( createInitTestModel("delete"), + createInitTestModel("deleteLock"), ) } } diff --git a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt index ab55334..c501f19 100644 --- a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt +++ b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt @@ -10,7 +10,10 @@ import kotlin.test.assertIs abstract class RepoAdUpdateTest { abstract val repo: IRepoAd protected open val updateSucc = initObjects[0] + protected open val updateConc = initObjects[1] protected val updateIdNotFound = MkplAdId("ad-repo-update-not-found") + protected val lockBad = MkplAdLock("20000000-0000-0000-0000-000000000009") + protected val lockNew = MkplAdLock("20000000-0000-0000-0000-000000000002") private val reqUpdateSucc by lazy { MkplAd( @@ -20,6 +23,7 @@ abstract class RepoAdUpdateTest { ownerId = MkplUserId("owner-123"), visibility = MkplVisibility.VISIBLE_TO_GROUP, adType = MkplDealSide.SUPPLY, + lock = initObjects.first().lock, ) } private val reqUpdateNotFound = MkplAd( @@ -29,7 +33,19 @@ abstract class RepoAdUpdateTest { ownerId = MkplUserId("owner-123"), visibility = MkplVisibility.VISIBLE_TO_GROUP, adType = MkplDealSide.SUPPLY, + lock = initObjects.first().lock, ) + private val reqUpdateConc by lazy { + MkplAd( + id = updateConc.id, + title = "update object not found", + description = "update object not found description", + ownerId = MkplUserId("owner-123"), + visibility = MkplVisibility.VISIBLE_TO_GROUP, + adType = MkplDealSide.SUPPLY, + lock = lockBad, + ) + } @Test fun updateSuccess() = runRepoTest { @@ -39,6 +55,7 @@ abstract class RepoAdUpdateTest { assertEquals(reqUpdateSucc.title, result.data.title) assertEquals(reqUpdateSucc.description, result.data.description) assertEquals(reqUpdateSucc.adType, result.data.adType) + assertEquals(lockNew, result.data.lock) } @Test @@ -49,9 +66,19 @@ abstract class RepoAdUpdateTest { assertEquals("id", error?.field) } + @Test + fun updateConcurrencyError() = runRepoTest { + val result = repo.updateAd(DbAdRequest(reqUpdateConc)) + assertIs(result) + val error = result.errors.find { it.code == "repo-concurrency" } + assertEquals("lock", error?.field) + assertEquals(updateConc, result.data) + } + companion object : BaseInitAds("update") { override val initObjects: List = listOf( createInitTestModel("update"), + createInitTestModel("updateConc"), ) } } diff --git a/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt b/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt index 4e8eefc..f08a216 100644 --- a/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt +++ b/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt @@ -11,7 +11,7 @@ object MkplAdStubBolts { ownerId = MkplUserId("user-1"), adType = MkplDealSide.DEMAND, visibility = MkplVisibility.VISIBLE_PUBLIC, - lock = MkplAdLock("123"), + lock = MkplAdLock("123-234-abc-ABC"), permissionsClient = mutableSetOf( MkplAdPermissionClient.READ, MkplAdPermissionClient.UPDATE,